Merge pull request #76 from liosha84/35-1-angular-17-logout-when-token-is-expired-like-as-bezkoder

Add fileName property to models and update related components for fil…
This commit is contained in:
liosha84
2025-08-09 22:27:41 +03:00
committed by GitHub
64 changed files with 1644 additions and 433 deletions
+76 -283
View File
@@ -19,20 +19,19 @@
"@angular/platform-server": "^20.0.0",
"@angular/router": "^20.0.0",
"@angular/ssr": "^20.0.2",
"@ant-design/icons-angular": "19.0.0",
"@hreimer/angular-image-viewer": "^0.14.1",
"@fortawesome/angular-fontawesome": "^3.0.0",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@ng-bootstrap/ng-bootstrap": "19.0.0",
"@popperjs/core": "2.11.8",
"@primeng/themes": "^19.1.3",
"angular-markdown-editor": "^3.1.1",
"bootstrap": "5.3.6",
"cropperjs": "^2.0.1",
"cropperjs": "^1.5.12",
"express": "^5.1.0",
"file-saver": "^2.0.5",
"ngx-filesaver": "^20.0.0",
"ngx-image-cropper": "^9.1.5",
"ngx-markdown": "^20.0.0",
"ngx-scrollbar": "18.0.0",
"primeng": "^19.1.3",
"prismjs": "^1.30.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@@ -947,43 +946,6 @@
}
}
},
"node_modules/@ant-design/colors": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz",
"integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
"license": "MIT",
"dependencies": {
"@ant-design/fast-color": "^2.0.6"
}
},
"node_modules/@ant-design/fast-color": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
"integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.7"
},
"engines": {
"node": ">=8.x"
}
},
"node_modules/@ant-design/icons-angular": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/@ant-design/icons-angular/-/icons-angular-19.0.0.tgz",
"integrity": "sha512-bBWFA1cTZwLAFTgpozkeNIHX1nXyZuiUaRzTcAfFEt85eW1X3ypMcBfS/XEVVVzkdTw5Td+E1vwzgfuUlKiYSA==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^7.0.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/common": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/platform-browser": "^19.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@antfu/install-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
@@ -2570,15 +2532,6 @@
"@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
@@ -2688,126 +2641,6 @@
"node": ">=0.1.90"
}
},
"node_modules/@cropper/element": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/element/-/element-2.0.1.tgz",
"integrity": "sha512-Jn1hR7XWzWQM/QfXRGMGzdkJ2gG/UcLdQPZQ7OKs0JiFfRzKpzu4u/nYrXHeH3MM2iOslLqh2kqYju6mjZLMJQ==",
"license": "MIT",
"dependencies": {
"@cropper/utils": "^2.0.1"
}
},
"node_modules/@cropper/element-canvas": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/element-canvas/-/element-canvas-2.0.1.tgz",
"integrity": "sha512-OKxq/O0HL9W2JegOsc2zh1NRpERZcLM5+M8aQ/eXdmMcfi1lzosPftag3Irp6pTsVpwV6B6ypIxKESzJ4ci9Fw==",
"license": "MIT",
"dependencies": {
"@cropper/element": "^2.0.1",
"@cropper/utils": "^2.0.1"
}
},
"node_modules/@cropper/element-crosshair": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/element-crosshair/-/element-crosshair-2.0.1.tgz",
"integrity": "sha512-bS5msU9cTU/jf1/kDw+QJmEM9/rw8IgOdpolR85iMVUCR8sRcLa0wgom42MBHcpBYB6hvL5YfiOeXZ7lHIYMpw==",
"license": "MIT",
"dependencies": {
"@cropper/element": "^2.0.1",
"@cropper/utils": "^2.0.1"
}
},
"node_modules/@cropper/element-grid": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/element-grid/-/element-grid-2.0.1.tgz",
"integrity": "sha512-ayqCvYQJ+GVT31HhFpttzHabW1T/LsIwLJY5PLTMG0cEZLw/E8ihg8mxctjZbo852D7oEePbz6/2SeuCb1018Q==",
"license": "MIT",
"dependencies": {
"@cropper/element": "^2.0.1",
"@cropper/utils": "^2.0.1"
}
},
"node_modules/@cropper/element-handle": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/element-handle/-/element-handle-2.0.1.tgz",
"integrity": "sha512-fdifyyPIaR9S2eQ7qPHuM8fX8uToAfBsi8vQlR9EM+oJkDNil0uO4rWyArLWEtlr0/q7U0OvsufcuJ7ffqfmpg==",
"license": "MIT",
"dependencies": {
"@cropper/element": "^2.0.1",
"@cropper/utils": "^2.0.1"
}
},
"node_modules/@cropper/element-image": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/element-image/-/element-image-2.0.1.tgz",
"integrity": "sha512-gPj5Sl2T8Cno198Cz3F3TDfcYoALW3yJ3fV6PHXmhMnX8sBkL7J441do7Vwkg0mEd2CogCtTLAf+p7ljdV0kgA==",
"license": "MIT",
"dependencies": {
"@cropper/element": "^2.0.1",
"@cropper/element-canvas": "^2.0.1",
"@cropper/utils": "^2.0.1"
}
},
"node_modules/@cropper/element-selection": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/element-selection/-/element-selection-2.0.1.tgz",
"integrity": "sha512-atv+Aeq2N2eWawelIRPGh1kYFdNrpb0QkUPPheGxz1ImfxpLdcHO9gb9T5noQijizUW2G0pNvts4ZaITQ0I71Q==",
"license": "MIT",
"dependencies": {
"@cropper/element": "^2.0.1",
"@cropper/element-canvas": "^2.0.1",
"@cropper/element-image": "^2.0.1",
"@cropper/utils": "^2.0.1"
}
},
"node_modules/@cropper/element-shade": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/element-shade/-/element-shade-2.0.1.tgz",
"integrity": "sha512-YIYgJ690NdFQ6wJLRFh/EySNVxGFKArncQ4FrsJ3yHU+ShgtOKz4FpjFLpqJRJB9swoVbD3WKTimGyzXrwjZrQ==",
"license": "MIT",
"dependencies": {
"@cropper/element": "^2.0.1",
"@cropper/element-canvas": "^2.0.1",
"@cropper/element-selection": "^2.0.1",
"@cropper/utils": "^2.0.1"
}
},
"node_modules/@cropper/element-viewer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/element-viewer/-/element-viewer-2.0.1.tgz",
"integrity": "sha512-HDj25l08pWi/AO6El/OqfQHBpBC4Lh5NEnQN1SOldsmxEwt27Ubv6ndDsF8LkTK7XPwjjZRpyQPyfig4w8L2JQ==",
"license": "MIT",
"dependencies": {
"@cropper/element": "^2.0.1",
"@cropper/element-canvas": "^2.0.1",
"@cropper/element-image": "^2.0.1",
"@cropper/element-selection": "^2.0.1",
"@cropper/utils": "^2.0.1"
}
},
"node_modules/@cropper/elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/elements/-/elements-2.0.1.tgz",
"integrity": "sha512-paFbBLXTKXNngn1yDi2ZIf+FO1pIEQXyBntmqOjuxqtG73KuEKv633wsJPFpj958bgcfSakgBbF80j+3nHbPug==",
"license": "MIT",
"dependencies": {
"@cropper/element": "^2.0.1",
"@cropper/element-canvas": "^2.0.1",
"@cropper/element-crosshair": "^2.0.1",
"@cropper/element-grid": "^2.0.1",
"@cropper/element-handle": "^2.0.1",
"@cropper/element-image": "^2.0.1",
"@cropper/element-selection": "^2.0.1",
"@cropper/element-shade": "^2.0.1",
"@cropper/element-viewer": "^2.0.1"
}
},
"node_modules/@cropper/utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@cropper/utils/-/utils-2.0.1.tgz",
"integrity": "sha512-A9RnAFmgNF5aZk5q2VZnFnHtXWu1kPyEN0LVsX8wJ2LBRu2nyETKwz+ZXVsVWliktToCaYojHKrS+6/HODyEZA==",
"license": "MIT"
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
@@ -3243,78 +3076,84 @@
"node": ">=18"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "5.15.4",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz",
"integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==",
"hasInstallScript": true,
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"optional": true,
"node_modules/@fortawesome/angular-fontawesome": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-3.0.0.tgz",
"integrity": "sha512-+8Dd6DoJnqArfrZ5NvjHyRL64IIkTigXclbOOcFdYQ8/WFERQUDaEU6SAV8Q0JBpJhMS1McED7YCOCAE6SIVyA==",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.0.0",
"tslib": "^2.8.1"
},
"peerDependencies": {
"@angular/core": "^20.0.0"
}
},
"node_modules/@fortawesome/angular-fontawesome/node_modules/@fortawesome/fontawesome-common-types": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.0.tgz",
"integrity": "sha512-PGMrIYXLGA5K8RWy8zwBkd4vFi4z7ubxtet6Yn13Plf6krRTwPbdlCwlcfmoX0R7B4Z643QvrtHmdQ5fNtfFCg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@hreimer/angular-image-viewer": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/@hreimer/angular-image-viewer/-/angular-image-viewer-0.14.1.tgz",
"integrity": "sha512-ZVV2xizunKqqlLewrBgshJJnfJ5IdyngwtNWj8c8tnHCFaR4xh6afL02NQthAYxv2rKMcaE7zkL6ZiR7tc6tCQ==",
"node_modules/@fortawesome/angular-fontawesome/node_modules/@fortawesome/fontawesome-svg-core": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.0.0.tgz",
"integrity": "sha512-obBEF+zd98r/KtKVW6A+8UGWeaOoyMpl6Q9P3FzHsOnsg742aXsl8v+H/zp09qSSu/a/Hxe9LNKzbBaQq1CEbA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
"@fortawesome/fontawesome-common-types": "7.0.0"
},
"optionalDependencies": {
"@angular/cdk": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^13.0.0",
"@angular/material": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^13.0.0",
"@fortawesome/fontawesome-free": "^5.15.4"
},
"peerDependencies": {
"@angular/common": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0",
"@angular/core": "^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0"
"engines": {
"node": ">=6"
}
},
"node_modules/@hreimer/angular-image-viewer/node_modules/@angular/cdk": {
"version": "13.3.9",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.9.tgz",
"integrity": "sha512-XCuCbeuxWFyo3EYrgEYx7eHzwl76vaWcxtWXl00ka8d+WAOtMQ6Tf1D98ybYT5uwF9889fFpXAPw98mVnlo3MA==",
"node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
"integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.3.0"
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"optionalDependencies": {
"parse5": "^5.0.0"
},
"peerDependencies": {
"@angular/common": "^13.0.0 || ^14.0.0-0",
"@angular/core": "^13.0.0 || ^14.0.0-0",
"rxjs": "^6.5.3 || ^7.4.0"
"engines": {
"node": ">=6"
}
},
"node_modules/@hreimer/angular-image-viewer/node_modules/@angular/material": {
"version": "13.3.9",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-13.3.9.tgz",
"integrity": "sha512-FU8lcMgo+AL8ckd27B4V097ZPoIZNRHiCe3wpgkImT1qC0YwcyXZVn0MqQTTFSdC9a/aI8wPm3AbTClJEVw5Vw==",
"license": "MIT",
"optional": true,
"node_modules/@fortawesome/free-brands-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
"integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"tslib": "^2.3.0"
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"peerDependencies": {
"@angular/animations": "^13.0.0 || ^14.0.0-0",
"@angular/cdk": "13.3.9",
"@angular/common": "^13.0.0 || ^14.0.0-0",
"@angular/core": "^13.0.0 || ^14.0.0-0",
"@angular/forms": "^13.0.0 || ^14.0.0-0",
"@angular/platform-browser": "^13.0.0 || ^14.0.0-0",
"rxjs": "^6.5.3 || ^7.4.0"
"engines": {
"node": ">=6"
}
},
"node_modules/@hreimer/angular-image-viewer/node_modules/parse5": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
"license": "MIT",
"optional": true
"node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@iconify/types": {
"version": "2.0.0",
@@ -7717,14 +7556,10 @@
}
},
"node_modules/cropperjs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-2.0.1.tgz",
"integrity": "sha512-hiJwk2SCPZqxMA7aR3byzLpYUqOrQo+ihMk8k/WRm/xe/LX8wNzAIzMwEB/NEGJYA6sbewxW9TUlrRUYi/2Ipg==",
"license": "MIT",
"dependencies": {
"@cropper/elements": "^2.0.1",
"@cropper/utils": "^2.0.1"
}
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz",
"integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
@@ -9259,12 +9094,6 @@
}
}
},
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
"license": "MIT"
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -12204,17 +12033,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/ngx-filesaver": {
"version": "20.0.0",
"resolved": "https://registry.npmjs.org/ngx-filesaver/-/ngx-filesaver-20.0.0.tgz",
"integrity": "sha512-84vRFGko1BmpyerAmLLggjx5ZiHvaWadJKlb2n0hOZlnzlTNZeI3zHKY9vqtIRkxCX70vWHTB8JwV5VvvkB2uQ==",
"node_modules/ngx-image-cropper": {
"version": "9.1.5",
"resolved": "https://registry.npmjs.org/ngx-image-cropper/-/ngx-image-cropper-9.1.5.tgz",
"integrity": "sha512-I8MAjMREtXDALgYyeMIvcqE+WpXA4IA9Z59JsujDuqOnjFY/Mpb+8JUyVVuOa0Irp20ukDPQJZmECR4v96hTrg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@types/file-saver": "^2.0.0",
"file-saver": "^2.0.0"
"@angular/common": ">=17.3.0",
"@angular/core": ">=17.3.0"
}
},
"node_modules/ngx-markdown": {
@@ -12241,21 +12070,6 @@
"zone.js": "~0.15.0"
}
},
"node_modules/ngx-scrollbar": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/ngx-scrollbar/-/ngx-scrollbar-18.0.0.tgz",
"integrity": "sha512-+ykmY491x+nzXvnecJvZHvDz0YWuX1r7SYMxNG4RVHXm5Z68P/8kd/3ryLD6DXdNWmJawd4NGvqq2ZkUKb/g3A==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/cdk": ">=19.0.0",
"@angular/common": ">=19.0.0",
"@angular/core": ">=19.0.0",
"rxjs": ">=7.0.0"
}
},
"node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
@@ -13241,27 +13055,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/primeng": {
"version": "19.1.3",
"resolved": "https://registry.npmjs.org/primeng/-/primeng-19.1.3.tgz",
"integrity": "sha512-KsrvJFblKg28kA6d4npnnABjKClmJv0CgDT/kOxFq5onQNBy4547DJzRGMba4+CMLKjHWWkYWuC+XSkPMNFrZg==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@primeuix/styled": "^0.3.2",
"@primeuix/utils": "^0.3.2",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/animations": "^19.0.0",
"@angular/cdk": "^19.0.0",
"@angular/common": "^19.0.0",
"@angular/core": "^19.0.0",
"@angular/forms": "^19.0.0",
"@angular/platform-browser": "^19.0.0",
"@angular/router": "^19.0.0",
"rxjs": "^6.0.0 || ^7.8.1"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
+6 -1
View File
@@ -24,13 +24,18 @@
"@angular/platform-server": "^20.0.0",
"@angular/router": "^20.0.0",
"@angular/ssr": "^20.0.2",
"@fortawesome/angular-fontawesome": "^3.0.0",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@ng-bootstrap/ng-bootstrap": "19.0.0",
"@popperjs/core": "2.11.8",
"@primeng/themes": "^19.1.3",
"angular-markdown-editor": "^3.1.1",
"bootstrap": "5.3.6",
"cropperjs": "^2.0.1",
"express": "^5.1.0",
"ngx-image-cropper": "^9.1.5",
"ngx-markdown": "^20.0.0",
"prismjs": "^1.30.0",
"rxjs": "~7.8.0",
+1 -1
View File
@@ -4,6 +4,6 @@
<title>Layer 1</title>
<text font-weight="bold" xml:space="preserve"
text-anchor="start" font-family="'JetBrains Mono'"
font-size="24" id="svg_1" y="27.49999" x="2.59999" stroke-width="0" stroke="#000" fill="#000000">jambotron</text>
font-size="24" fileName="svg_1" y="27.49999" x="2.59999" stroke-width="0" stroke="#000" fill="#000000">jambotron</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 344 B

+1 -7
View File
@@ -9,7 +9,6 @@ import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import {AppModule} from './app.module';
import {providePrimeNG} from 'primeng/config';
import Aura from '@primeng/themes/aura';
import {provideAnimations} from '@angular/platform-browser/animations';
import {provideMarkdown} from 'ngx-markdown';
@@ -24,11 +23,6 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes), provideClientHydration(withEventReplay()),
provideMarkdown(),
importProvidersFrom(AppModule),
providePrimeNG({
theme: {
preset: Aura
}
})
importProvidersFrom(AppModule)
]
};
+25 -2
View File
@@ -1,6 +1,10 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {Router, RouterOutlet} from '@angular/router';
import {SpinnerComponent} from './components/spinner/spinner.component';
import {EventBusService} from './_shared/event-bus.service';
import {AuthService} from './services/auth.service';
import {TokenStorageService} from './services/token-storage.service';
import {GlobalConstants} from './global-constants';
@Component({
selector: 'app-root',
@@ -15,7 +19,26 @@ import {SpinnerComponent} from './components/spinner/spinner.component';
export class App {
protected title = 'jambotron-ui';
constructor() {
constructor(
private eventBusService:EventBusService,
private authService:AuthService,
private tokenStorageService:TokenStorageService,
private router: Router
) {
this.eventBusService.on('logout', () => {
console.log('eventBusService logout');
this.authService.logout().subscribe({
next: res => {
console.log(res);
this.tokenStorageService.clean();
router.navigate([GlobalConstants.DEFAULT_PAGE]).then();
},
error: err => {
console.log(err);
}
});
})
}
}
@@ -14,7 +14,7 @@
</mat-toolbar>
<input
type="file"
id="fileInput"
fileName="fileInput"
(change)="selectFile($event)"
name="fileInput"
/>
@@ -0,0 +1,103 @@
<!--
<div class="ngx-pe-overlay-container">
<div class="ngx-pe-dialog-container" [style.max-width]="modalMaxWidth">
<div class="ngx-pe-dialog-header" *ngIf="!hideModalHeader">
{{modalTitle}}
</div>
<div class="ngx-pe-dialog-body">
<div class="ngx-pe-cropper-wrapper">
<div class="ngx-pe-loading" *ngIf="!imageLoaded" [class.ngx-pe-processing]="isProcessing">
<svg xmlns="http://www.w3.org/2000/svg"
style="margin: auto; display: block; shape-rendering: auto; animation-play-state: running; animation-delay: 0s;"
viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" fill="none" stroke="#f3f3f3" stroke-width="10" r="35"
stroke-dasharray="164.93361431346415 56.97787143782138"
style="animation-play-state: running; animation-delay: 0s;">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s"
values="0 50 50;360 50 50" keyTimes="0;1"
style="animation-play-state: running; animation-delay: 0s;"></animateTransform>
</circle>
</svg>
</div>
<img class="ngx-pe-img" #image loading="lazy" (load)="onLoad()" (error)="error()" [src]="imgUrl" alt="cropper-img" crossorigin="anonymous">
<div class="ngx-pe-tool-bar">
&lt;!&ndash;move&ndash;&gt;
<button (click)="move()" title="Move">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M24 44 15.85 35.85 18.05 33.65 22.5 38.1V28.65H25.5V38.1L29.95 33.65L32.15 35.85ZM11.9 31.9 4 24 11.95 16.05 14.15 18.25 9.9 22.5H19.35V25.5H9.9L14.1 29.7ZM36.1 31.9 33.9 29.7 38.1 25.5H28.7V22.5H38.1L33.9 18.3L36.1 16.1L44 24ZM22.5 19.3V9.9L18.3 14.1L16.1 11.9L24 4L31.9 11.9L29.7 14.1L25.5 9.9V19.3Z"/>
</svg>
</button>
&lt;!&ndash;crop&ndash;&gt;
<button (click)="crop()" title="Crop">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M34.75 31.75V13.25Q34.75 13.25 34.75 13.25Q34.75 13.25 34.75 13.25H16.25V10.25H34.75Q35.95 10.25 36.85 11.15Q37.75 12.05 37.75 13.25V31.75ZM36.25 46Q35.6 46 35.175 45.575Q34.75 45.15 34.75 44.5V37.75H13.25Q12.05 37.75 11.15 36.85Q10.25 35.95 10.25 34.75V13.25H3.5Q2.85 13.25 2.425 12.825Q2 12.4 2 11.75Q2 11.1 2.425 10.675Q2.85 10.25 3.5 10.25H10.25V3.5Q10.25 2.85 10.675 2.425Q11.1 2 11.75 2Q12.4 2 12.825 2.425Q13.25 2.85 13.25 3.5V34.75Q13.25 34.75 13.25 34.75Q13.25 34.75 13.25 34.75H44.5Q45.15 34.75 45.575 35.175Q46 35.6 46 36.25Q46 36.9 45.575 37.325Q45.15 37.75 44.5 37.75H37.75V44.5Q37.75 45.15 37.325 45.575Q36.9 46 36.25 46Z"/>
</svg>
</button>
&lt;!&ndash;zoom in&ndash;&gt;
<button (click)="zoomIn()" title="Zoom In">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M38.7 40.85 26.65 28.8Q25.15 30.1 23.15 30.825Q21.15 31.55 18.9 31.55Q13.5 31.55 9.75 27.8Q6 24.05 6 18.75Q6 13.45 9.75 9.7Q13.5 5.95 18.85 5.95Q24.15 5.95 27.875 9.7Q31.6 13.45 31.6 18.75Q31.6 20.9 30.9 22.9Q30.2 24.9 28.8 26.65L40.95 38.7Q41.4 39.1 41.375 39.75Q41.35 40.4 40.9 40.85Q40.45 41.3 39.8 41.3Q39.15 41.3 38.7 40.85ZM18.85 28.55Q22.9 28.55 25.75 25.675Q28.6 22.8 28.6 18.75Q28.6 14.7 25.75 11.825Q22.9 8.95 18.85 8.95Q14.75 8.95 11.875 11.825Q9 14.7 9 18.75Q9 22.8 11.875 25.675Q14.75 28.55 18.85 28.55ZM18.8 24.3Q18.15 24.3 17.725 23.875Q17.3 23.45 17.3 22.8V20.2H14.65Q14.05 20.2 13.625 19.75Q13.2 19.3 13.2 18.65Q13.2 18.05 13.625 17.625Q14.05 17.2 14.7 17.2H17.3V14.6Q17.3 14 17.75 13.575Q18.2 13.15 18.85 13.15Q19.45 13.15 19.875 13.575Q20.3 14 20.3 14.65V17.2H22.9Q23.5 17.2 23.925 17.65Q24.35 18.1 24.35 18.7Q24.35 19.35 23.925 19.775Q23.5 20.2 22.85 20.2H20.3V22.85Q20.3 23.45 19.85 23.875Q19.4 24.3 18.8 24.3Z"/>
</svg>
</button>
&lt;!&ndash;zoom out&ndash;&gt;
<button (click)="zoomOut()" title="Zoom Out">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M38.7 40.85 26.65 28.8Q25.15 30.1 23.15 30.825Q21.15 31.55 18.9 31.55Q13.5 31.55 9.75 27.8Q6 24.05 6 18.75Q6 13.45 9.75 9.7Q13.5 5.95 18.85 5.95Q24.15 5.95 27.875 9.7Q31.6 13.45 31.6 18.75Q31.6 20.9 30.9 22.9Q30.2 24.9 28.8 26.65L40.95 38.7Q41.4 39.1 41.375 39.75Q41.35 40.4 40.9 40.85Q40.45 41.3 39.8 41.3Q39.15 41.3 38.7 40.85ZM18.85 28.55Q22.9 28.55 25.75 25.675Q28.6 22.8 28.6 18.75Q28.6 14.7 25.75 11.825Q22.9 8.95 18.85 8.95Q14.75 8.95 11.875 11.825Q9 14.7 9 18.75Q9 22.8 11.875 25.675Q14.75 28.55 18.85 28.55ZM15.2 20.2Q14.6 20.2 14.175 19.75Q13.75 19.3 13.75 18.65Q13.75 18.05 14.175 17.625Q14.6 17.2 15.25 17.2H22.35Q22.95 17.2 23.375 17.65Q23.8 18.1 23.8 18.7Q23.8 19.35 23.375 19.775Q22.95 20.2 22.3 20.2Z"/>
</svg>
</button>
&lt;!&ndash;rotate left&ndash;&gt;
<button (click)="rotateLeft()" title="Rotate Left">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M7.9 23.6Q7.15 23.6 6.7 23.075Q6.25 22.55 6.4 21.85Q6.75 20.3 7.325 18.85Q7.9 17.4 8.7 16.05Q9.1 15.4 9.775 15.35Q10.45 15.3 11 15.85Q11.4 16.2 11.45 16.75Q11.5 17.3 11.25 17.75Q10.55 18.9 10.075 20.075Q9.6 21.25 9.35 22.4Q9.25 22.95 8.85 23.275Q8.45 23.6 7.9 23.6ZM20.05 43.7Q18.6 43.35 17.15 42.775Q15.7 42.2 14.4 41.4Q13.75 41 13.7 40.3Q13.65 39.6 14.2 39Q14.55 38.65 15.05 38.6Q15.55 38.55 15.95 38.8Q17.1 39.5 18.225 39.975Q19.35 40.45 20.55 40.75Q21.05 40.85 21.4 41.275Q21.75 41.7 21.75 42.25Q21.75 43 21.25 43.425Q20.75 43.85 20.05 43.7ZM8.7 35.7Q7.9 34.4 7.325 32.925Q6.75 31.45 6.4 29.85Q6.25 29.15 6.675 28.625Q7.1 28.1 7.9 28.1Q8.4 28.1 8.825 28.425Q9.25 28.75 9.35 29.3Q9.6 30.55 10.05 31.725Q10.5 32.9 11.2 33.95Q11.5 34.4 11.45 34.95Q11.4 35.5 11 35.9Q10.45 36.45 9.775 36.4Q9.1 36.35 8.7 35.7ZM27.95 43.65Q27.25 43.85 26.75 43.425Q26.25 43 26.25 42.25Q26.25 41.75 26.6 41.325Q26.95 40.9 27.45 40.75Q32.5 39.4 35.75 35.375Q39 31.35 39 25.85Q39 19.5 34.675 15.175Q30.35 10.85 24 10.85H23L25.85 13.7Q26.3 14.15 26.3 14.8Q26.3 15.45 25.85 15.9Q25.4 16.35 24.75 16.35Q24.1 16.35 23.65 15.9L18.15 10.4Q17.9 10.15 17.8 9.9Q17.7 9.65 17.7 9.35Q17.7 9.05 17.8 8.8Q17.9 8.55 18.15 8.3L23.65 2.8Q24.1 2.35 24.75 2.35Q25.4 2.35 25.85 2.8Q26.3 3.25 26.3 3.9Q26.3 4.55 25.85 5L23 7.85H24Q31.55 7.85 36.775 13.075Q42 18.3 42 25.85Q42 32.4 38.075 37.275Q34.15 42.15 27.95 43.65Z"/>
</svg>
</button>
&lt;!&ndash;rotate right&ndash;&gt;
<button (click)="rotateRight()" title="Rotate Right">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M40.2 23.6Q39.7 23.6 39.275 23.25Q38.85 22.9 38.75 22.4Q38.5 21.2 38.025 20.05Q37.55 18.9 36.85 17.75Q36.6 17.3 36.65 16.75Q36.7 16.2 37.1 15.8Q37.65 15.25 38.35 15.325Q39.05 15.4 39.45 16.05Q40.25 17.35 40.8 18.775Q41.35 20.2 41.7 21.8Q41.85 22.5 41.4 23.05Q40.95 23.6 40.2 23.6ZM26.3 42.2Q26.3 41.7 26.65 41.275Q27 40.85 27.5 40.75Q28.65 40.5 29.8 40.025Q30.95 39.55 32.1 38.85Q32.55 38.6 33.075 38.625Q33.6 38.65 34 39.05Q34.55 39.6 34.475 40.3Q34.4 41 33.75 41.4Q32.4 42.25 30.975 42.8Q29.55 43.35 28.1 43.7Q27.4 43.85 26.85 43.4Q26.3 42.95 26.3 42.2ZM37.1 35.9Q36.75 35.55 36.7 34.975Q36.65 34.4 36.9 33.95Q37.6 32.8 38.05 31.65Q38.5 30.5 38.75 29.3Q38.85 28.8 39.25 28.45Q39.65 28.1 40.2 28.1Q40.95 28.1 41.375 28.625Q41.8 29.15 41.65 29.85Q41.25 31.45 40.7 32.9Q40.15 34.35 39.4 35.65Q39 36.3 38.325 36.375Q37.65 36.45 37.1 35.9ZM20.05 43.7Q13.9 42.3 10 37.35Q6.1 32.4 6.1 25.85Q6.1 18.3 11.325 13.075Q16.55 7.85 24.1 7.85H25.1L22.25 5Q21.8 4.55 21.8 3.9Q21.8 3.25 22.25 2.8Q22.7 2.35 23.35 2.35Q24 2.35 24.45 2.8L29.95 8.3Q30.2 8.55 30.3 8.8Q30.4 9.05 30.4 9.35Q30.4 9.65 30.3 9.9Q30.2 10.15 29.95 10.4L24.45 15.9Q24 16.35 23.35 16.35Q22.7 16.35 22.25 15.9Q21.8 15.45 21.8 14.8Q21.8 14.15 22.25 13.7L25.1 10.85H24.1Q17.75 10.85 13.425 15.175Q9.1 19.5 9.1 25.85Q9.1 31.35 12.35 35.375Q15.6 39.4 20.65 40.7Q21.15 40.8 21.5 41.225Q21.85 41.65 21.85 42.2Q21.85 42.95 21.3 43.4Q20.75 43.85 20.05 43.7Z"/>
</svg>
</button>
&lt;!&ndash;flip vertical&ndash;&gt;
<button (click)="flipH()" title="Flip Horizontal">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M9 42Q7.8 42 6.9 41.1Q6 40.2 6 39V9Q6 7.8 6.9 6.9Q7.8 6 9 6H18.1Q18.75 6 19.175 6.425Q19.6 6.85 19.6 7.5Q19.6 8.15 19.175 8.575Q18.75 9 18.1 9H9Q9 9 9 9Q9 9 9 9V39Q9 39 9 39Q9 39 9 39H18.1Q18.75 39 19.175 39.425Q19.6 39.85 19.6 40.5Q19.6 41.15 19.175 41.575Q18.75 42 18.1 42ZM24.1 46Q23.45 46 23.025 45.575Q22.6 45.15 22.6 44.5V3.5Q22.6 2.85 23.025 2.425Q23.45 2 24.1 2Q24.75 2 25.175 2.425Q25.6 2.85 25.6 3.5V44.5Q25.6 45.15 25.175 45.575Q24.75 46 24.1 46ZM39 9H38.3V6H39Q40.2 6 41.1 6.9Q42 7.8 42 9V9.7H39ZM39 26.75V21.25H42V26.75ZM39 42H38.3V39H39V38.3H42V39Q42 40.2 41.1 41.1Q40.2 42 39 42ZM39 18.25V12.7H42V18.25ZM39 35.3V29.75H42V35.3ZM28.6 42V39H35.3V42ZM28.6 9V6H35.3V9Z"/>
</svg>
</button>
&lt;!&ndash;flip horizontal&ndash;&gt;
<button (click)="flipV()" title="Flip Vertical">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" transform='rotate(90)'>
<path
d="M9 42Q7.8 42 6.9 41.1Q6 40.2 6 39V9Q6 7.8 6.9 6.9Q7.8 6 9 6H18.1Q18.75 6 19.175 6.425Q19.6 6.85 19.6 7.5Q19.6 8.15 19.175 8.575Q18.75 9 18.1 9H9Q9 9 9 9Q9 9 9 9V39Q9 39 9 39Q9 39 9 39H18.1Q18.75 39 19.175 39.425Q19.6 39.85 19.6 40.5Q19.6 41.15 19.175 41.575Q18.75 42 18.1 42ZM24.1 46Q23.45 46 23.025 45.575Q22.6 45.15 22.6 44.5V3.5Q22.6 2.85 23.025 2.425Q23.45 2 24.1 2Q24.75 2 25.175 2.425Q25.6 2.85 25.6 3.5V44.5Q25.6 45.15 25.175 45.575Q24.75 46 24.1 46ZM39 9H38.3V6H39Q40.2 6 41.1 6.9Q42 7.8 42 9V9.7H39ZM39 26.75V21.25H42V26.75ZM39 42H38.3V39H39V38.3H42V39Q42 40.2 41.1 41.1Q40.2 42 39 42ZM39 18.25V12.7H42V18.25ZM39 35.3V29.75H42V35.3ZM28.6 42V39H35.3V42ZM28.6 9V6H35.3V9Z"/>
</svg>
</button>
<button (click)="reset()" title="Reset">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path
d="M9.8 31.45Q8.8 29.65 8.4 27.825Q8 26 8 24.1Q8 17.55 12.725 12.825Q17.45 8.1 24 8.1H26.15L22.15 4.1L24.1 2.15L31.55 9.6L24.1 17.05L22.1 15.05L26.05 11.1H24Q18.65 11.1 14.825 14.925Q11 18.75 11 24.1Q11 25.55 11.275 26.85Q11.55 28.15 11.95 29.3ZM23.8 46 16.35 38.55 23.8 31.1 25.75 33.05 21.75 37.05H24Q29.35 37.05 33.175 33.225Q37 29.4 37 24.05Q37 22.6 36.75 21.3Q36.5 20 36 18.85L38.15 16.7Q39.15 18.5 39.575 20.325Q40 22.15 40 24.05Q40 30.6 35.275 35.325Q30.55 40.05 24 40.05H21.75L25.75 44.05Z"/>
</svg>
</button>
</div>
</div>
</div>
<div class="ngx-pe-dialog-footer">
<button class="ngx-pe-btn ngx-pe-btn-outline-light" (click)="close()">{{closeBtnText}}</button>
<button class="ngx-pe-btn ngx-pe-btn-primary" (click)="export()">{{applyBtnText}}</button>
</div>
</div>
</div>
-->
@@ -0,0 +1,179 @@
@import "~node_modules/cropperjs/dist/cropper.css";
.ngx-pe-overlay-container {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.25);
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 99999;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
padding: 1rem;
box-sizing: border-box;
}
.ngx-pe-overlay-container * {
box-sizing: border-box;
}
.ngx-pe-dialog-container {
position: relative;
border-radius: 4px;
overflow: hidden;
outline: 0;
width: 100%;
height: auto;
max-height: 95vh;
box-shadow: 0 11px 15px -7px #0003, 0 24px 38px 3px #00000024, 0 9px 46px 8px #0000001f;
background: #333333;
color: white;
display: flex;
flex-direction: column;
}
.ngx-pe-dialog-header {
padding: 1rem 1rem 1rem 1rem;
font-size: 1rem;
font-weight: 400;
}
.ngx-pe-dialog-body {
height: 100%;
position: relative;
box-sizing: border-box;
}
.ngx-pe-dialog-footer {
padding: 1rem 1rem;
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
.ngx-pe-cropper-wrapper {
position: relative;
}
.ngx-pe-img {
display: block;
max-width: 100%;
height: auto;
max-height: 300px;
}
.ngx-pe-tool-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
background-color: #222;
box-sizing: border-box;
width: 100%;
}
.ngx-pe-tool-bar button {
background-color: transparent;
border-width: 0;
color: #fff;
cursor: pointer;
float: left;
font-size: 0.875rem;
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
width: 2.5rem;
}
.ngx-pe-tool-bar button:hover {
background-color: #0074d9;
color: #fff;
}
.ngx-pe-tool-bar svg {
width: 20px;
height: 20px;
fill: white;
}
.ngx-pe-btn {
--bs-btn-padding-y: 0.25rem;
--bs-btn-padding-x: 1rem;
--bs-btn-font-size: 0.875rem;
--bs-btn-border-radius: 0.25rem;
--bs-btn-font-weight: 400;
--bs-btn-line-height: 1.5;
--bs-btn-color: #212529;
--bs-btn-bg: transparent;
--bs-btn-border-width: 1px;
--bs-btn-border-color: transparent;
--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
--bs-btn-disabled-opacity: 0.65;
--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb));
display: inline-block;
padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
font-size: var(--bs-btn-font-size);
font-weight: var(--bs-btn-font-weight);
line-height: var(--bs-btn-line-height);
color: var(--bs-btn-color);
text-align: center;
text-decoration: none;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
border-radius: var(--bs-btn-border-radius);
background-color: var(--bs-btn-bg);
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.ngx-pe-btn:hover {
color: var(--bs-btn-hover-color);
background-color: var(--bs-btn-hover-bg);
border-color: var(--bs-btn-hover-border-color);
}
.ngx-pe-btn-outline-light {
--bs-btn-color: #f8f9fa;
--bs-btn-border-color: #f8f9fa;
--bs-btn-hover-color: #000;
--bs-btn-hover-bg: #f8f9fa;
--bs-btn-hover-border-color: #f8f9fa;
--bs-btn-focus-shadow-rgb: 248, 249, 250;
--bs-btn-active-color: #000;
--bs-btn-active-bg: #f8f9fa;
--bs-btn-active-border-color: #f8f9fa;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #f8f9fa;
--bs-btn-disabled-bg: transparent;
--bs-gradient: none;
}
.ngx-pe-btn-primary {
--bs-btn-color: #fff;
--bs-btn-bg: #0d6efd;
--bs-btn-border-color: #0d6efd;
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #0b5ed7;
--bs-btn-hover-border-color: #0a58ca;
--bs-btn-focus-shadow-rgb: 49, 132, 253;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #0a58ca;
--bs-btn-active-border-color: #0a53be;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #0d6efd;
--bs-btn-disabled-border-color: #0d6efd;
}
.ngx-pe-loading {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 2;
background-color: #222222;
display: flex;
justify-content: center;
align-items: center;
transition: all 10s ease;
}
.ngx-pe-processing {
background-color: rgba(34,34,34, 0.8);
}
.ngx-pe-loading svg {
width: 50px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PhotoEditComponent } from './photo-edit.component';
describe('PhotoEditComponent', () => {
let component: PhotoEditComponent;
let fixture: ComponentFixture<PhotoEditComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [PhotoEditComponent]
})
.compileComponents();
fixture = TestBed.createComponent(PhotoEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,228 @@
/*
import {Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild} from '@angular/core';
import Cropper from "cropperjs";
import ViewMode = Cropper.ViewMode;
import {NgIf, NgOptimizedImage} from '@angular/common';
@Component({
selector: 'app-photo-edit.component',
imports: [
NgIf,
NgOptimizedImage
],
templateUrl: './photo-edit.component.html',
styleUrl: './photo-edit.component.scss'
})
export class PhotoEditComponent implements OnDestroy {
@ViewChild('image')
image!: ElementRef;
private cropper!: Cropper;
public imgUrl!: string;
private FILES_REGEX = /\.(gif|jpe?g|tiff|png|webp|bmp)$/i;
@Input() modalTitle:String = 'Photo Editor';
@Input() hideModalHeader = false;
@Input() aspectRatio = NaN;
@Input() autoCropArea = 0.8;
@Input() autoCrop = true;
@Input() mask = true;
@Input() guides = true;
@Input() centerIndicator = true;
@Input() viewMode:ViewMode = 0;
@Input() modalMaxWidth = '500px';
@Input() modalCentered = false;
@Input() scalable = true;
@Input() zoomable = true;
@Input() cropBoxMovable = true;
@Input() cropBoxResizable = true;
@Input() darkTheme = true;
@Input() roundCropper = false;
@Input() canvasHeight = 400;
@Input() resizeToWidth: number | any;
@Input() resizeToHeight: number | any;
@Input() imageSmoothingEnabled = true;
@Input() imageSmoothingQuality: ImageSmoothingQuality = 'high';
@Input() format: string | any;
@Input() closeBtnText = 'Close';
@Input() applyBtnText = 'Apply';
quality = 92;
@Input() set imageQuality(value: number) {
if (value > 0 && value <= 100) {
this.quality = value;
} else {
this.errorEvent.error('Invalid image quality');
}
}
@Output() closeEvent = new EventEmitter<any>();
@Output() imageCroppedEvent = new EventEmitter<PhotoEditCroppedEvent>();
@Output() errorEvent = new EventEmitter<any>();
imageLoaded = false;
isProcessing = false;
@Input() set source(data: File | string | any) {
if (data instanceof Event) {
// @ts-ignore
const file = (<HTMLInputElement>data.target).files[0];
if (this.FILES_REGEX.test(file.name)) {
if (!this.format) {
// @ts-ignore
this.format = data.target.files[0].type.split('/')[1];
}
const reader = new FileReader();
reader.onload = (ev: any) => {
this.imgUrl = ev.target.result;
};
// @ts-ignore
reader.readAsDataURL(data.target.files[0]);
} else {
console.log('error');
this.errorEvent.emit('Not supported INPUT');
}
} else if (data instanceof File) {
if (this.FILES_REGEX.test(data.name)) {
if (!this.format) {
this.format = data.type.split('/')[1];
}
const reader = new FileReader();
reader.onload = (ev: any) => {
this.imgUrl = ev.target.result;
};
reader.readAsDataURL(data);
} else {
this.errorEvent.emit('Not supported INPUT');
}
} else if (typeof data === 'string') {
if ((/^data:image\/([a-zA-Z]*);base64,([^\"]*)$/).test(data)) {
this.imgUrl = data;
if (!this.format) {
this.format = ((data.split(',')[0]).split(';')[0]).split(':')[1].split('/')[1];
}
} else if (this.isValidImageURL(data)) {
this.imgUrl = data;
if (!this.format) {
// @ts-ignore
this.format = data.split(/[#?]/)[0].split('.').pop().trim();
}
} else {
this.errorEvent.emit('Not supported URL');
}
} else {
this.errorEvent.emit('Not supported INPUT');
}
}
isValidImageURL(str: string) {
return str.match(/^http[^\?]*.(jpg|jpeg|gif|png|tiff|bmp)(\?(.*))?(\#(.*))?$/gim);
}
rotateRight() {
this.cropper.rotate(45);
}
rotateLeft() {
this.cropper.rotate(-45);
}
crop() {
this.cropper.setDragMode('crop');
}
move() {
this.cropper.setDragMode('move');
}
zoomIn() {
this.cropper.zoom(0.1);
}
zoomOut() {
this.cropper.zoom(-0.1);
}
flipH() {
this.cropper.scaleX(-this.cropper.getImageData().scaleX);
}
flipV() {
this.cropper.scaleY(-this.cropper.getImageData().scaleY);
}
reset() {
this.cropper.reset();
}
export() {
this.isProcessing = true;
this.imageLoaded = false;
let cropedImage = this.cropper.getCroppedCanvas({
width: this.resizeToWidth,
height: this.resizeToHeight,
imageSmoothingEnabled: this.imageSmoothingEnabled,
imageSmoothingQuality: this.imageSmoothingQuality
});
const outputImage = cropedImage.toDataURL('image/' + this.format, this.quality);
cropedImage.toBlob((blob: any) => {
this.imageCroppedEvent.emit({
base64: outputImage,
file: new File([blob],
Date.now() + '.' + this.format,
{type: 'image/' + this.format})
});
this.isProcessing = false;
this.imageLoaded = true;
}, 'image/' + this.format, this.quality / 100);
}
close() {
this.closeEvent.emit();
}
onLoad() {
this.image.nativeElement.addEventListener('ready', () => {
this.imageLoaded = true;
});
this.cropper = new Cropper(this.image.nativeElement, {
aspectRatio: this.aspectRatio,
autoCropArea: this.autoCropArea,
autoCrop: this.autoCrop,
modal: this.mask, // black mask
guides: this.guides, // grid
center: this.centerIndicator, // center indicator
viewMode: this.viewMode,
scalable: this.scalable,
zoomable: this.zoomable,
background: false,
cropBoxMovable: this.cropBoxMovable,
cropBoxResizable: this.cropBoxResizable,
});
}
ngOnDestroy(): void {
console.log('destroyed');
}
error() {
this.errorEvent.emit('Error loading image');
}
}
export interface PhotoEditCroppedEvent {
base64?: string;
file?: File;
}
export type imageFormat = 'gif' | 'jpeg' | 'tiff' | 'png' | 'webp' | 'bmp';
*/
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { PhotoEditorService } from './photo-editor.service';
describe('PhotoEditorService', () => {
let service: PhotoEditorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(PhotoEditorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,92 @@
/*
import {
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
EmbeddedViewRef,
Injectable,
Injector, ViewContainerRef
} from '@angular/core';
import {Observable, Subject} from 'rxjs';
import {PhotoEditComponent, PhotoEditCroppedEvent} from './photo-edit.component';
import ViewMode = Cropper.ViewMode;
@Injectable({
providedIn: 'root'
})
export class PhotoEditorService {
private photoEditorSubscriber!: Subject<any>;
private photoEditorComponentRef!: ComponentRef<PhotoEditComponent>;
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private appRef: ApplicationRef,
private injector: Injector) {
}
open(source: Event | string | File | any, options?: Options): Observable<PhotoEditCroppedEvent> {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(PhotoEditComponent);
const componentRef = componentFactory.create(this.injector);
this.appRef.attachView(componentRef.hostView);
const domElem = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
document.body.appendChild(domElem);
this.photoEditorSubscriber = new Subject<string>();
this.photoEditorComponentRef = componentRef;
this.photoEditorComponentRef.instance.closeEvent.subscribe(() => this.close());
this.photoEditorComponentRef.instance.errorEvent.subscribe((data) => this.errorHandler(data));
this.photoEditorComponentRef.instance.imageCroppedEvent.subscribe((data) => this.export(data));
if (options) {
Object.keys(options).map(value => {
// @ts-ignore
this.photoEditorComponentRef.instance[value] = options[value];
})
}
this.photoEditorComponentRef.instance.source = source;
return this.photoEditorSubscriber.asObservable();
}
private errorHandler(data: any) {
this.photoEditorSubscriber.error(data);
this.close();
}
private close() {
this.photoEditorSubscriber.complete();
this.photoEditorComponentRef.destroy();
}
private export(data: any) {
this.photoEditorSubscriber.next(data);
this.close();
}
}
export interface Options {
aspectRatio?: number | any;
modalTitle?: string;
hideModalHeader?: boolean;
autoCropArea?: number;
autoCrop?: boolean;
mask?: boolean;
guides?: boolean;
centerIndicator?: boolean;
viewMode?: ViewMode;
modalMaxWidth?: string;
scalable?: boolean;
zoomable?: boolean;
cropBoxMovable?: boolean;
cropBoxResizable?: boolean;
roundCropper?: boolean;
resizeToWidth?: number | any;
resizeToHeight?: number | any;
imageSmoothingEnabled?: boolean;
imageSmoothingQuality?: ImageSmoothingQuality;
format?: string | any;
imageQuality?: number;
applyBtnText?: string;
closeBtnText?: string;
}
*/
@@ -1,5 +1,5 @@
@if (isSpinnerVisible) {
<div id="http-loader">
<div fileName="http-loader">
<div class="loader-bg">
@if (spinner() === Spinkit.skLine) {
<div class="sk-line-material" [class.colored]="!backgroundColor()">
@@ -24,9 +24,9 @@ export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// req = req.clone({
// withCredentials: true,
// });
req = req.clone({
withCredentials: true,
});
return next.handle(req).pipe(
catchError((error) => {
@@ -59,6 +59,7 @@ export class AuthInterceptor implements HttpInterceptor {
if (error.status == '403') {
this.eventBusService.emit(new EventData('logout', null));
console.log('eventBusService logout');
}
return throwError(() => error);
+2
View File
@@ -1,4 +1,6 @@
export class FileInfo {
name: string | undefined;
url: string | undefined;
mode: string | undefined;//View or CropView
base64string: string | undefined;
}
+1 -1
View File
@@ -1,4 +1,4 @@
export class Role {
id?:any;
fileName?:any;
name?:string;
}
+1 -1
View File
@@ -1,5 +1,5 @@
export class Setting {
id?:any;
fileName?:any;
name?:string;
value?:boolean;
}
@@ -1,6 +1,6 @@
export class Tutorial {
isSelected?: boolean;
id?: any;
fileName?: any;
title?: string;
description?: string;
published?: boolean;
+1 -1
View File
@@ -1,5 +1,5 @@
export class User {
id?:any;
fileName?:any;
username?:string;
email?:string;
password?:string;
@@ -14,7 +14,7 @@ export class AdminModuleService {
}
updateUserRoles(id: any,data: User): Observable<any> {
return this.http.put(`${this.baseUrl}/users/${id}`, data);
updateUserRoles(fileName: any,data: User): Observable<any> {
return this.http.put(`${this.baseUrl}/users/${fileName}`, data);
}
}
@@ -1,14 +1,11 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit} from '@angular/core';
import {Router, RouterOutlet} from "@angular/router";
import {DOCUMENT} from '@angular/common';
import { ScrollPanelModule } from 'primeng/scrollpanel';
import {MenuItem} from 'primeng/api';
import {SideBarAdminComponent} from '../side-bar-admin.component/side-bar-admin.component';
@Component({
selector: 'app-admin.component',
imports: [
RouterOutlet,
ScrollPanelModule,
SideBarAdminComponent
],
schemas:[CUSTOM_ELEMENTS_SCHEMA],
@@ -19,9 +16,7 @@ export class AdminComponent implements OnInit {
private document = inject<Document>(DOCUMENT);
height = 0;
items: MenuItem[] | undefined;
constructor(private router: Router) {
constructor(private router: Router) {
this.height = 0
}
@@ -66,30 +61,7 @@ export class AdminComponent implements OnInit {
else
this.height = 0;
this.items = [
{
label: 'Admin',
icon: 'pi pi-palette',
command: () => {
this.router.navigate(['/main/admin/welcome']);
},
items: [
{
label: 'Settings',
icon: 'pi pi-eraser',
// route: '/main/admin/settings'
command: () => {
this.router.navigate(['/main/admin/settings']);
}
},
{
label: 'System',
icon: 'pi pi-heart',
route: '/main/admin/system'
}
]
}
];
}
}
@@ -1,15 +1,13 @@
import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from '@angular/core';
import {mainRouting} from '../main-module/main.routing';
import {MainComponent} from '../main-module/main.component/main.component';
import {RouterModule} from '@angular/router';
import {AdminComponent} from './admin.component/admin.component';
import {adminRouting} from './admin.routing';
import { ScrollPanelModule } from 'primeng/scrollpanel';
@NgModule({
declarations: [
],
imports: [adminRouting, AdminComponent,ScrollPanelModule],
imports: [adminRouting, AdminComponent],
exports: [RouterModule],
schemas:[CUSTOM_ELEMENTS_SCHEMA]
})
@@ -85,7 +85,7 @@ export class UsersComponent {
}
saveUserRoles(user:User){
this.adminService.updateUserRoles(user.id, user).subscribe(
this.adminService.updateUserRoles(user.fileName, user).subscribe(
(data:any) => {
console.log(data);
user.isEdit = false
@@ -98,13 +98,13 @@ export class UsersComponent {
isChecked(element: User, role: Role) {
let userRoles: Role[] = element.roles;
return element.roles.some((r:Role) => r.id === role.id);
return element.roles.some((r:Role) => r.fileName === role.fileName);
}
onValChange(element:User, role: Role) {
let userRoles: Role[] = element.roles;
if (this.isChecked(element, role)) {
userRoles = userRoles.filter((r:Role) => r.id !== role.id);
userRoles = userRoles.filter((r:Role) => r.fileName !== role.fileName);
}else {
userRoles.push(role);
}
@@ -29,9 +29,9 @@
<mat-toolbar class="image-toolbar">
<mat-toolbar-row>
<span class="menu-spacer"></span>
<!-- <button matMiniFab matTooltip="Dawnload image" aria-label="Download" (click)="downloadImage(image.url)">
<button matMiniFab matTooltip="Dawnload image" aria-label="Download" (click)="downloadImage(image.url)">
<mat-icon>download</mat-icon>
</button>-->
</button>
@if (isLoggedIn) {
<button matMiniFab matTooltip="Save to server" aria-label="Save" (click)="save(image.url)" >
<mat-icon>upload_file</mat-icon>
@@ -19,7 +19,7 @@
color: white;
position: absolute;
left : 50%;
top: 5%;
top: 42px;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
padding: 0px 2px;
@@ -89,6 +89,18 @@ export class GenerateImageComponent {
console.log(error);
});
}
downloadImage(url: string | undefined) {
const link = document.createElement('a');
link.setAttribute('target', '_blank');
link.setAttribute('download', 'image.png');
// @ts-ignore
link.download = url;
// @ts-ignore
link.href = url;
link.click();
}
}
@@ -19,12 +19,12 @@ export class ModeratorApiService {
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`);
}
getTutorial(id: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${id}`);
getTutorial(fileName: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${fileName}`);
}
publish(id: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${id}`, data);
publish(fileName: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${fileName}`, data);
}
}
@@ -55,18 +55,18 @@ const language = 'typescript';
### Blockquote
> Blockquote to the max`;
private id: string | null | undefined;
private fileName: string | null | undefined;
constructor(private moderatorApiService: ModeratorApiService, private route: ActivatedRoute) {
this.route.queryParams
.subscribe(params => {
console.log(params);
this.id = params['id'];
console.log(this.id);
this.fileName = params['fileName'];
console.log(this.fileName);
});
this.moderatorApiService.getTutorial(this.id).subscribe(
this.moderatorApiService.getTutorial(this.fileName).subscribe(
data=>{
this.tutorial = data;
}
@@ -75,7 +75,7 @@ const language = 'typescript';
publishTutorial(): void {
this.tutorial.published = true;
this.moderatorApiService.publish(this.id, this.tutorial)
this.moderatorApiService.publish(this.fileName, this.tutorial)
.subscribe(
response => {
console.log(response);
@@ -9,7 +9,7 @@
@case('isEdit') {
<div class="btn-edit" >
<button mat-button routerLink='../tutorial-preview' [queryParams]="{id:element.id}" (click)="element.isEdit = !element.isEdit">
<button mat-button routerLink='../tutorial-preview' [queryParams]="{fileName:element.fileName}" (click)="element.isEdit = !element.isEdit">
Preview
</button>
@@ -78,7 +78,7 @@ export class TutorialsListComponent {
publish(element: any, checked: boolean){
element.published = checked;
this.moderatorApiService.publish(element.id, element).subscribe(
this.moderatorApiService.publish(element.fileName, element).subscribe(
response => {
console.log(response);
},
@@ -1,5 +1,5 @@
<div class="tutorial-view">
<img style="width: 600px; height: 500px;" src="{{tutorial.titleimage}}" alt="">
<img style="width: 716px; height: 400px;" src="{{tutorial.titleimage}}" alt="">
<markdown class="tutorial-body" [data]="tutorial.body"></markdown>
</div>
@@ -14,7 +14,7 @@ import {MarkdownComponent} from 'ngx-markdown';
styleUrl: './tutorial-view.component.scss'
})
export class TutorialViewComponent {
private id: string | null | undefined;
private fileName: string | null | undefined;
tutorial: Tutorial = new Tutorial();
constructor(private tutorialsApiService: TutorialsApiService, private route: ActivatedRoute) {
@@ -22,11 +22,11 @@ export class TutorialViewComponent {
this.route.queryParams
.subscribe(params => {
console.log(params);
this.id = params['id'];
console.log(this.id);
this.fileName = params['fileName'];
console.log(this.fileName);
});
this.tutorialsApiService.getTutorial(this.id).subscribe(
this.tutorialsApiService.getTutorial(this.fileName).subscribe(
data => {
this.tutorial = data;
}
@@ -17,6 +17,6 @@ export class TutorialsApiService {
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`);
}
getTutorial(id: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${id}`);
getTutorial(fileName: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${fileName}`);
}}
@@ -4,7 +4,7 @@
<img mat-card-image [src]="tutorial.titleimage" alt="Photo of a" />
<mat-card-actions>
<button mat-button routerLink="../tutorial-view" [queryParams]="{id:tutorial.id}" target="_blank">{{tutorial.title}}</button>
<button mat-button routerLink="../tutorial-view" [queryParams]="{fileName:tutorial.fileName}" target="_blank">{{tutorial.title}}</button>
</mat-card-actions>
</mat-card>
@@ -4,12 +4,13 @@
.container-card-view{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(358px, 0fr));
gap: 24px;
width: 100%;
height: 200px;
object-fit: cover;
padding: 24px;
justify-content: center;
//width: 100%;
//height: 100%;
//padding-top: 20px;
@@ -26,13 +27,14 @@
}
img {
width: 100%;
width: 358px;
height: 200px;
object-fit: cover;
}
.responsive-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(358px, 0fr));
gap: 24px;
}
@@ -0,0 +1,19 @@
<p>dialog-cropp-image.component works!</p>
<mat-dialog-content class="mat-typography">
<image-cropper
[imageURL]="image"
style="width: 400px; height: 300px;"
[imageChangedEvent]="imageChangedEvent"
[maintainAspectRatio]="true"
[aspectRatio]="4 / 3"
format="png"
(imageCropped)="imageCropped($event)"
(imageLoaded)="imageLoaded($event)"
(cropperReady)="cropperReady()"
(loadImageFailed)="loadImageFailed()"
></image-cropper>
<img style="width: 358px; height: 200px" [src]="croppedImage" />
</mat-dialog-content>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DialogCroppImageComponent } from './dialog-cropp-image.component';
describe('DialogCroppImageComponent', () => {
let component: DialogCroppImageComponent;
let fixture: ComponentFixture<DialogCroppImageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DialogCroppImageComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DialogCroppImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,62 @@
import {Component, inject, input} from '@angular/core';
import {ImageCroppedEvent, ImageCropperComponent, LoadedImage} from 'ngx-image-cropper';
import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {UserApiService} from '../user-api.service';
import {MAT_DIALOG_DATA, MatDialogContent} from '@angular/material/dialog';
@Component({
selector: 'app-dialog-cropp-image.component',
imports: [
ImageCropperComponent,
MatDialogContent
],
templateUrl: './dialog-cropp-image.component.html',
styleUrl: './dialog-cropp-image.component.scss'
})
export class DialogCroppImageComponent {
imageChangedEvent: Event | null = null;
croppedImage: SafeUrl = '';
image?: string;
imageCropUrl: string | undefined;
constructor(
private userApiService: UserApiService,
private sanitizer: DomSanitizer,
) {
this.cropImage(this.imageCropUrl);
}
///------------
fileChangeEvent(event: Event): void {
this.imageChangedEvent = event;
}
imageCropped(event: ImageCroppedEvent) {
if (event.objectUrl != null) {
this.croppedImage = this.sanitizer.bypassSecurityTrustUrl(event.objectUrl);
}
// event.blob can be used to upload the cropped image
}
imageLoaded(image: LoadedImage) {
// show cropper
}
cropperReady() {
// cropper ready
}
loadImageFailed() {
// show message
}
cropImage(url: string | undefined) {
if(url !== undefined){
this.userApiService.loadImageAsBase64(url).subscribe(data => {
this.image = data;
})
}
}
}
@@ -1,25 +1,122 @@
<p>images.component works!</p>
<div class="generate-image-view">
@for (fileInfo of fileInfos; track fileInfo) {
<mat-card>
<mat-card-header>
<div class="image-container">
<img src="{{fileInfo.url}}"
alt="" width="100%" height="100%">
<mat-toolbar class="image-toolbar">
<mat-toolbar-row>
<label class="image-file-name">{{fileInfo.name}}</label>
<span class="menu-spacer"></span>
<button matMiniFab matTooltip="Dawnload image" aria-label="Download" (click)="downloadImage(fileInfo.url)">
<mat-icon>download</mat-icon>
</button>
@if(fileInfo.mode !=="CropView"){
<img src="{{fileInfo.url}}"
alt="" width="100%" height="100%"/>
}@else{
<image-cropper
output = "base64"
[imageURL]="fileInfo.base64string"
style="width: 100%; height: 100%;"
[imageChangedEvent]="imageChangedEvent"
[maintainAspectRatio]="true"
[aspectRatio]="4 / 3"
[canvasRotation]="canvasRotation"
[cropperStaticWidth]="cropperStaticWidth"
[cropperStaticHeight]="cropperStaticHeight"
[(transform)]="transform"
format="png"
(imageCropped)="imageCropped($event)"
(imageLoaded)="imageLoaded($event)"
(cropperReady)="cropperReady()"
(loadImageFailed)="loadImageFailed()"
></image-cropper>
</mat-toolbar-row>
</mat-toolbar>
</div>
<img #imagePreview width="100%" height="100%" [src]="croppedImage" />
}
@if(fileInfo.mode !=="CropView"){
<mat-toolbar class="image-toolbar">
<mat-toolbar-row>
<label class="image-file-name">{{fileInfo.name}}</label>
<span class="menu-spacer"></span>
<button matMiniFab matTooltip="Crop image" aria-label="Crop" (click)="cropImage(fileInfo)">
<mat-icon>photo_size_select_small</mat-icon>
</button>
<button matMiniFab matTooltip="Dawnload image" aria-label="Download" (click)="downloadImage(fileInfo.url)">
<mat-icon>download</mat-icon>
</button>
</mat-toolbar-row>
</mat-toolbar>
}@else{
<mat-toolbar class="crop-toolbar">
<!-- Zoom -->
<mat-toolbar-row>
<button matMiniFab matTooltip="Size +" aria-label="Crop" (click)="zoomIn()">
<mat-icon>zoom_in</mat-icon>
</button>
<button matMiniFab matTooltip="Size -" aria-label="Download" (click)="zoomOut()">
<mat-icon>zoom_out</mat-icon>
</button>
</mat-toolbar-row>
<!-- Move -->
<mat-toolbar-row class="move-toolbar-row">
<table class="move-toolbar-table">
<tr style="display: flex; justify-content: center;">
<th colspan="2" >
<button matMiniFab matTooltip="Move up" aria-label="Crop" (click)="moveUp()">
<mat-icon>north</mat-icon>
</button>
</th>
</tr>
<tr>
<td>
<button matMiniFab matTooltip="Move left" aria-label="Download" (click)="moveLeft()">
<mat-icon>west</mat-icon>
</button>
</td>
<td>
<button matMiniFab matTooltip="Move right" aria-label="Crop" (click)="moveRight()">
<mat-icon>east</mat-icon>
</button>
</td>
</tr>
<tr style="display: flex; justify-content: center;">
<th colspan="2" >
<button matMiniFab matTooltip="Move down" aria-label="Download" (click)="moveDown()">
<mat-icon>south</mat-icon>
</button>
</th>
</tr>
</table>
</mat-toolbar-row>
<!-- Rotate -->
<mat-toolbar-row>
<button matMiniFab matTooltip="Rotate left" aria-label="Crop" (click)="rotateLeft()">
<mat-icon>rotate_left</mat-icon>
</button>
<button matMiniFab matTooltip="Rotate right" aria-label="Download" (click)="rotateRight()">
<mat-icon>rotate_right</mat-icon>
</button>
</mat-toolbar-row>
<!-- Flip -->
<mat-toolbar-row>
<button matMiniFab matTooltip="Flip vertical" aria-label="Crop" (click)="flipHorizontal()">
<mat-icon>flip</mat-icon>
</button>
<button matMiniFab matTooltip="Flip horizontal" aria-label="Download" (click)="flipVertical()">
<mat-icon style="transform: rotate(90deg)">flip</mat-icon>
</button>
</mat-toolbar-row>
<mat-toolbar-row>
<mat-chip-listbox class="mat-mdc-chip-set-stacked" aria-label="Cutest dog breeds">
<mat-chip-option (selectionChange)="cropFixedSize($event)" >Crop 716x400</mat-chip-option>
</mat-chip-listbox>
<span class="menu-spacer"></span>
</mat-toolbar-row>
<mat-toolbar-row>
<button matMiniFab matTooltip="Save to server" aria-label="Save" (click)="save()" >
<mat-icon>upload_file</mat-icon>
</button>
</mat-toolbar-row>
</mat-toolbar>
}
</div>
</mat-card-header>
<mat-card-content>
</mat-card-content>
@@ -11,13 +11,15 @@
color: white;
position: absolute;
left : 50%;
top: 5%;
top: 47px;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
padding: 0px 2px;
border: none;
border-radius: 5px;
width: 100%;
}
.image-container .menu-spacer {
flex: 1 1 auto;
}
@@ -29,3 +31,33 @@
.mat-mdc-mini-fab{
margin: 5px;
}
.spacer {
flex: 1 1 auto;
}
.move-toolbar-row{
height: 140px;
}
.image-container .crop-toolbar{
background-color: rgba(153,153,153,0);
color: white;
position: absolute;
left : 59px;
top: 240px;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
padding: 0px 2px;
border: none;
border-radius: 5px;
width: 10%;
}
.move-toolbar-table{
display: flex;
justify-content: center;
flex-direction: column;
}
@@ -1,12 +1,16 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, inject} from '@angular/core';
import {Component, ElementRef, inject, ViewChild} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader} from '@angular/material/card';
import {MatMiniFabButton} from '@angular/material/button';
import {MatToolbar, MatToolbarRow} from '@angular/material/toolbar';
import {MatTooltip} from '@angular/material/tooltip';
import {MatIcon} from '@angular/material/icon';
import {FileInfo} from '../../../models/file-info';
import {UserApiService} from '../user-api.service';
import {Base64ImagePayload, UploadImageResponse, UserApiService} from '../user-api.service';
import {CropService, ImageCroppedEvent, ImageCropperComponent, ImageTransform, LoadedImage} from 'ngx-image-cropper';
import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {MatDialog} from '@angular/material/dialog';
import {DialogCroppImageComponent} from '../dialog-cropp-image.component/dialog-cropp-image.component';
import {MatChipListbox, MatChipOption, MatChipSelectionChange} from '@angular/material/chips';
@Component({
selector: 'app-images.component',
@@ -18,17 +22,49 @@ import {MatDialog} from '@angular/material/dialog';
MatMiniFabButton,
MatToolbar,
MatToolbarRow,
MatTooltip
MatTooltip,
ImageCropperComponent,
MatChipListbox,
MatChipOption
],
templateUrl: './images.component.html',
styleUrl: './images.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
styleUrl: './images.component.scss'
})
export class ImagesComponent {
fileInfos?: FileInfo[] = [];
readonly dialog = inject(MatDialog);
constructor(private userApiService: UserApiService) {
imageChangedEvent: Event | null = null;
croppedImage: string | null | undefined = '';
croppedImageBase64: string | null | undefined;
@ViewChild('imagePreview') imagePreview!: ElementRef;
canvasRotation = 0;
transform: ImageTransform = {
translateUnit: 'px',
scale: 1,
rotate: 0,
flipH: false,
flipV: false,
translateH: 0,
translateV: 0
};
cropperStaticWidth = 0;
cropperStaticHeight = 0;
loading = false;
currentCroppedFile: FileInfo | undefined;
constructor(
private userApiService: UserApiService,
private sanitizer: DomSanitizer,
) {
this.userApiService.getImages().subscribe(data =>{
this.fileInfos = data;
console.log(data);
@@ -36,6 +72,214 @@ export class ImagesComponent {
}
downloadImage(url: string | undefined) {
const link = document.createElement('a');
// @ts-ignore
link.download = url;
// @ts-ignore
link.href = url;
link.click();
}
openCropImageDialog(imageUrl: string | undefined, enterAnimationDuration: string, exitAnimationDuration: string) {
let dialogCropImageRef = this.dialog.open(DialogCroppImageComponent, {
height: '500px',
width: '800px',
enterAnimationDuration,
exitAnimationDuration,
}
);
dialogCropImageRef.componentInstance.cropImage(imageUrl);
// dialogCropImageRef.componentInstance.uploadClicked.subscribe(result => {
// dialogCropImageRef.close();
// this.openUploadImageDialog(enterAnimationDuration, exitAnimationDuration);
// })
// const dialogSelectSubscription = dialogCropImageRef.componentInstance.selectClicked
// .subscribe(result => {
// console.log('Got the data!', result);
//
// if (result == null) {
// return;
// }
// this.tutorial.titleimage = result.url;
//
// });
}
cropImage(fileInfo: FileInfo) {
// close the crop view for a previous cropped file
if(this.currentCroppedFile !== undefined){
this.currentCroppedFile.mode = "View";
}
fileInfo.mode = "CropView";
this.setBase64String(fileInfo);
this.currentCroppedFile = fileInfo;
}
setBase64String(fileInfo:FileInfo) {
if(fileInfo.url !== undefined){
this.userApiService.loadImageAsBase64(fileInfo.url).subscribe(data => {
fileInfo.base64string = data;
})
}
}
imageCropped(event: ImageCroppedEvent) {
if (event.objectUrl != null) {
// this.croppedImage = this.sanitizer.bypassSecurityTrustUrl(event.objectUrl);
}
else {
this.croppedImageBase64 = event.base64;
this.croppedImage = this.croppedImageBase64;
}
this.imagePreview.nativeElement.height = event.height;
this.imagePreview.nativeElement.width = event.width;
}
imageLoaded(image: LoadedImage) {
// show cropper
}
cropperReady() {
// cropper ready
}
loadImageFailed() {
// show message
}
zoomIn() {
this.transform = {
...this.transform,
scale: this.transform.scale! + .1
};
}
zoomOut() {
this.transform = {
...this.transform,
scale: this.transform.scale! - .1
};
}
moveLeft() {
this.transform = {
...this.transform,
translateH: this.transform.translateH! - 1
};
}
moveRight() {
this.transform = {
...this.transform,
translateH: this.transform.translateH! + 1
};
}
moveDown() {
this.transform = {
...this.transform,
translateV: this.transform.translateV! + 1
};
}
moveUp() {
this.transform = {
...this.transform,
translateV: this.transform.translateV! - 1
};
}
flipHorizontal() {
this.transform = {
...this.transform,
flipH: !this.transform.flipH
};
}
flipVertical() {
this.transform = {
...this.transform,
flipV: !this.transform.flipV
};
}
rotateLeft() {
this.loading = true;
setTimeout(() => { // Use timeout because rotating image is a heavy operation and will block the ui thread
this.canvasRotation--;
this.flipAfterRotate();
});
}
rotateRight() {
this.loading = true;
setTimeout(() => {
this.canvasRotation++;
this.flipAfterRotate();
});
}
private flipAfterRotate() {
const flippedH = this.transform.flipH;
const flippedV = this.transform.flipV;
this.transform = {
...this.transform,
flipH: flippedV,
flipV: flippedH,
translateH: 0,
translateV: 0
};
}
cropFixedSize($event: MatChipSelectionChange) {
if ($event.source.selected) {
this.cropperStaticWidth = 716;
this.cropperStaticHeight = 400;
}
else {
this.cropperStaticWidth = 0;
this.cropperStaticHeight = 0;
}
}
save() {
const payload = this.getBase64ImagePayload();
this.userApiService.uploadUserBase64Image(payload).subscribe({
next: (res:UploadImageResponse) =>{
const fileInfo: FileInfo = {
name: res.fileName,
url: res.url,
mode: "View",
base64string: undefined
}
this.fileInfos?.push(fileInfo);
console.log("Uploaded:", res);
} ,
error: (err) => console.error("Upload failed", err),
});
}
private getBase64ImagePayload(): Base64ImagePayload {
const match = this.croppedImageBase64?.match(/^data:(.+);base64,(.*)$/);
if(match)
{
const [, mime, base64] = match;
return {
mime: mime,
data: base64
};
}
else {
throw new Error("Invalid base64 string");
}
}
}
@@ -70,7 +70,7 @@
<mat-divider></mat-divider>
<mat-tab-group>
<mat-tab label="Markdown text | Result">
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
<textarea class="variable-textarea" [(ngModel)]="tutorial.body"></textarea>
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
</mat-tab>
<mat-tab label="Editor">
@@ -70,7 +70,7 @@ const language = 'typescript';
### Blockquote
> Blockquote to the max`;
private id: string | null | undefined;
private fileName: string | null | undefined;
constructor(private userApiService: UserApiService,
private route: ActivatedRoute,
@@ -81,11 +81,11 @@ const language = 'typescript';
this.route.queryParams
.subscribe(params => {
console.log(params);
this.id = params['id'];
console.log(this.id);
this.fileName = params['fileName'];
console.log(this.fileName);
});
this.userApiService.getTutorial(this.id).subscribe(
this.userApiService.getTutorial(this.fileName).subscribe(
data=>{
this.tutorial = data;
}
@@ -148,7 +148,7 @@ const language = 'typescript';
}
updateTutorial(): void {
this.userApiService.update(this.id, this.tutorial)
this.userApiService.update(this.fileName, this.tutorial)
.subscribe(
response => {
console.log(response);
@@ -37,13 +37,13 @@
}
@case('isEdit') {
<div class="btn-edit" >
<button mat-button routerLink='../tutorial-edit' [queryParams]="{id:element.id}" (click)="element.isEdit = !element.isEdit">
<button mat-button routerLink='../tutorial-edit' [queryParams]="{fileName:element.fileName}" (click)="element.isEdit = !element.isEdit">
Edit
</button>
<button
mat-button
class="button-remove"
(click)="removeRow(element.id)"
(click)="removeRow(element.fileName)"
>
Delete
</button>
@@ -98,7 +98,7 @@
<button
mat-button
class="button-remove"
(click)="removeRow(element.id)"
(click)="removeRow(element.fileName)"
>
Delete
</button>
@@ -116,7 +116,7 @@
<button
mat-button
(click)="editRow(element)"
[disabled]="disableSubmit(element.id)"
[disabled]="disableSubmit(element.fileName)"
>
Done
</button>
@@ -125,17 +125,17 @@ import {MatSlideToggle} from '@angular/material/slide-toggle';
// })
}
removeRow(id: number) {
this.userApiService.deleteTutorial(id).subscribe(() => {
removeRow(fileName: number) {
this.userApiService.deleteTutorial(fileName).subscribe(() => {
this.dataSource.data = this.dataSource.data.filter(
(u: Tutorial) => u.id !== id,
(u: Tutorial) => u.fileName !== fileName,
)
})
}
publish(element: any, checked: boolean){
element.tobepublished = checked;
this.userApiService.update(element.id, element).subscribe(
this.userApiService.update(element.fileName, element).subscribe(
response => {
console.log(response);
},
@@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import {forkJoin, Observable} from 'rxjs';
import {forkJoin, Observable, switchMap} from 'rxjs';
import {Tutorial} from '../../models/tutorial.model';
import {HttpClient, HttpEvent, HttpHeaders, HttpRequest} from '@angular/common/http';
import {GlobalConstants} from '../../global-constants';
import {FileInfo} from '../../models/file-info';
import {map} from 'rxjs/operators';
@Injectable({
providedIn: 'root'
@@ -15,6 +16,11 @@ export class UserApiService {
}
uploadUserBase64Image(base64Image: Base64ImagePayload): Observable<any> {
return this.http.post<UploadImageResponse>(`${this.baseUrl}/user-images/upload-base64-image`, base64Image, { headers: { "Content-Type": "application/json" } }
);
}
upload(file: File): Observable<HttpEvent<any>> {
const formData: FormData = new FormData();
formData.append('file', file);
@@ -36,32 +42,59 @@ export class UserApiService {
return this.http.get<FileInfo[]>(`${this.baseUrl}/getImages`);
}
loadImageAsBase64(imageUrl: string): Observable<string> {
return this.http.post<Base64ImageResponse>(`${this.baseUrl}/user-images/base64-image`, imageUrl)
.pipe(map(r => `data:${r.mime};base64,${(r.data || '').trim()}`));
}
getUserAllTutorials(): Observable<Tutorial[]> {
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`);
}
getTutorial(id: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${id}`);
getTutorial(fileName: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${fileName}`);
}
create(data: any): Observable<any> {
return this.http.post(`${this.baseUrl}/tutorial-add`, data);
}
update(id: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${id}`, data);
update(fileName: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${fileName}`, data);
}
deleteTutorials(tutorials: Tutorial[]): Observable<Tutorial[]> {
return forkJoin(
tutorials.map((tutorial) =>
this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${tutorial.id}`)
this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${tutorial.fileName}`)
)
);
}
deleteTutorial(id: number): Observable<Tutorial> {
return this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${id}`);
deleteTutorial(fileName: number): Observable<Tutorial> {
return this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${fileName}`);
}
}
export interface Base64ImagePayload {
mime: string; // e.g., "image/png"
data: string; // base64 string (no "data:" prefix)
}
export interface UploadImageResponse {
message: string;
fileName: string;
url: string; // URL to access the stored image
mime: string;
size: number; // bytes
}
export interface Base64ImageResponse {
message: string;
mime: string;
data: string; // Base64 without "data:" prefix
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 MiB

+2 -2
View File
@@ -4,11 +4,11 @@
<path d="M22.8814 13.2276L24.6545 15.0002L24.479 15.1757L24.4796 15.1763L26.8691 12.8646L25.0568 11.0522L25.0544 11.0498H24.8783L22.7565 13.1022L22.8814 13.2276Z" fill="url(#paint1_linear_112102_1824)"/>
<path d="M3.9497 11.0498L3.95211 11.0522L6.12693 13.2276L14.5041 21.6043L25.0586 11.0498H3.9497Z" fill="#1890FF"/>
<defs>
<linearGradient id="paint0_linear_112102_1824" x1="5.62978" y1="11.5889" x2="2.57161" y2="14.6471" gradientUnits="userSpaceOnUse">
<linearGradient fileName="paint0_linear_112102_1824" x1="5.62978" y1="11.5889" x2="2.57161" y2="14.6471" gradientUnits="userSpaceOnUse">
<stop stop-color="#023B95"/>
<stop offset="0.9637" stop-color="#096CD9" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_112102_1824" x1="23.2722" y1="11.6281" x2="25.7451" y2="14.4382" gradientUnits="userSpaceOnUse">
<linearGradient fileName="paint1_linear_112102_1824" x1="23.2722" y1="11.6281" x2="25.7451" y2="14.4382" gradientUnits="userSpaceOnUse">
<stop stop-color="#023B95"/>
<stop offset="1" stop-color="#096DD9" stop-opacity="0"/>
</linearGradient>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

+2 -2
View File
@@ -5,11 +5,11 @@
<path d="M6.9453 13.5496L6.94771 13.552L9.12253 15.7275L17.4998 24.1041L28.0542 13.5496H6.9453Z" fill="#1890FF"/>
<path d="M46.925 25.5V9.594H50.753L54.922 21.749L59.135 9.594H62.941V25.5H60.224V13.026L56.033 25.5H53.8L49.642 13.103V25.5H46.925ZM69.5711 25.72C68.8451 25.72 68.1924 25.588 67.6131 25.324C67.0411 25.0527 66.5901 24.6677 66.2601 24.169C65.9374 23.663 65.7761 23.0507 65.7761 22.332C65.7761 21.078 66.2271 20.132 67.1291 19.494C68.0384 18.856 69.4867 18.5223 71.4741 18.493L73.1681 18.449V17.679C73.1681 17.0777 72.9884 16.6083 72.6291 16.271C72.2771 15.9263 71.7271 15.7577 70.9791 15.765C70.4291 15.7723 69.9231 15.9043 69.4611 16.161C69.0064 16.4103 68.7057 16.832 68.5591 17.426H66.2821C66.3261 16.6487 66.5497 15.9997 66.9531 15.479C67.3637 14.951 67.9247 14.5587 68.6361 14.302C69.3474 14.038 70.1687 13.906 71.1001 13.906C72.1854 13.906 73.0691 14.049 73.7511 14.335C74.4331 14.621 74.9354 15.0353 75.2581 15.578C75.5807 16.1133 75.7421 16.7623 75.7421 17.525V25.5H73.4871L73.2781 23.454C72.8674 24.312 72.3467 24.906 71.7161 25.236C71.0927 25.5587 70.3777 25.72 69.5711 25.72ZM70.4401 23.905C70.7701 23.905 71.0964 23.8463 71.4191 23.729C71.7417 23.6043 72.0351 23.4393 72.2991 23.234C72.5631 23.0213 72.7721 22.7867 72.9261 22.53C73.0874 22.2733 73.1681 22.0057 73.1681 21.727V19.934L71.7711 19.956C71.0817 19.9633 70.4804 20.0367 69.9671 20.176C69.4537 20.3153 69.0504 20.539 68.7571 20.847C68.4711 21.1477 68.3281 21.551 68.3281 22.057C68.3281 22.6363 68.5297 23.091 68.9331 23.421C69.3364 23.7437 69.8387 23.905 70.4401 23.905ZM78.8735 25.5V14.126H81.4915V15.754C81.6675 15.4607 81.9058 15.1783 82.2065 14.907C82.5145 14.6357 82.8921 14.4157 83.3395 14.247C83.7868 14.0783 84.3111 13.994 84.9125 13.994C85.6165 13.994 86.2691 14.1333 86.8705 14.412C87.4791 14.6907 87.9668 15.127 88.3335 15.721C88.7075 16.315 88.8945 17.0813 88.8945 18.02V25.5H86.2105V18.339C86.2105 17.591 86.0051 17.0373 85.5945 16.678C85.1838 16.3113 84.6668 16.128 84.0435 16.128C83.6181 16.128 83.2148 16.2013 82.8335 16.348C82.4521 16.4873 82.1405 16.7037 81.8985 16.997C81.6638 17.283 81.5465 17.6423 81.5465 18.075V25.5H78.8735ZM97.652 16.018H95.111L95.122 22.541C95.122 22.8783 95.1587 23.1277 95.232 23.289C95.3127 23.443 95.4373 23.5457 95.606 23.597C95.782 23.641 96.0167 23.663 96.31 23.663H97.707V25.357C97.5603 25.4157 97.3367 25.467 97.036 25.511C96.7427 25.5623 96.3467 25.588 95.848 25.588C94.946 25.588 94.2457 25.4707 93.747 25.236C93.2557 24.994 92.9147 24.653 92.724 24.213C92.5333 23.773 92.438 23.2487 92.438 22.64V16.018H90.59V14.126H92.515L93.186 10.749H95.111V14.115H97.652V16.018ZM103.033 14.126V25.5H100.459V14.126H103.033ZM103.066 9.605V12.135H100.415V9.605H103.066ZM110.545 25.72C109.753 25.72 109.001 25.5953 108.29 25.346C107.586 25.0893 106.992 24.6897 106.508 24.147C106.031 23.6043 105.731 22.904 105.606 22.046H107.96C108.063 22.464 108.242 22.805 108.499 23.069C108.763 23.333 109.075 23.5273 109.434 23.652C109.793 23.7693 110.16 23.828 110.534 23.828C111.216 23.828 111.766 23.7107 112.184 23.476C112.609 23.2413 112.822 22.8783 112.822 22.387C112.822 22.0277 112.705 21.7417 112.47 21.529C112.235 21.3163 111.861 21.155 111.348 21.045L109.181 20.55C108.22 20.3373 107.447 19.9853 106.86 19.494C106.281 19.0027 105.987 18.3097 105.98 17.415C105.973 16.7403 106.141 16.139 106.486 15.611C106.831 15.083 107.344 14.6687 108.026 14.368C108.708 14.06 109.551 13.906 110.556 13.906C111.883 13.906 112.947 14.203 113.746 14.797C114.545 15.3837 114.956 16.2233 114.978 17.316H112.701C112.62 16.8247 112.389 16.4433 112.008 16.172C111.627 15.8933 111.132 15.754 110.523 15.754C109.892 15.754 109.375 15.875 108.972 16.117C108.569 16.359 108.367 16.7293 108.367 17.228C108.367 17.5727 108.521 17.844 108.829 18.042C109.137 18.24 109.599 18.4087 110.215 18.548L112.261 19.043C112.848 19.1897 113.328 19.3877 113.702 19.637C114.076 19.8863 114.369 20.1613 114.582 20.462C114.795 20.7553 114.941 21.0597 115.022 21.375C115.11 21.683 115.154 21.969 115.154 22.233C115.154 22.9663 114.963 23.5933 114.582 24.114C114.208 24.6273 113.676 25.0233 112.987 25.302C112.298 25.5807 111.484 25.72 110.545 25.72Z" fill="#262626"/>
<defs>
<linearGradient id="paint0_linear_114034_8280" x1="8.62538" y1="14.0888" x2="5.56721" y2="17.1469" gradientUnits="userSpaceOnUse">
<linearGradient fileName="paint0_linear_114034_8280" x1="8.62538" y1="14.0888" x2="5.56721" y2="17.1469" gradientUnits="userSpaceOnUse">
<stop stop-color="#023B95"/>
<stop offset="0.9637" stop-color="#096CD9" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_114034_8280" x1="26.2673" y1="14.1279" x2="28.7402" y2="16.938" gradientUnits="userSpaceOnUse">
<linearGradient fileName="paint1_linear_114034_8280" x1="26.2673" y1="14.1279" x2="28.7402" y2="16.938" gradientUnits="userSpaceOnUse">
<stop stop-color="#023B95"/>
<stop offset="1" stop-color="#096DD9" stop-opacity="0"/>
</linearGradient>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

+2 -2
View File
@@ -4,11 +4,11 @@
<path d="M22.8814 13.2276L24.6545 15.0002L24.479 15.1757L24.4796 15.1763L26.8691 12.8646L25.0568 11.0522L25.0544 11.0498H24.8783L22.7565 13.1022L22.8814 13.2276Z" fill="url(#paint1_linear_112102_1824)"/>
<path d="M3.9497 11.0498L3.95211 11.0522L6.12693 13.2276L14.5041 21.6043L25.0586 11.0498H3.9497Z" fill="#1890FF"/>
<defs>
<linearGradient id="paint0_linear_112102_1824" x1="5.62978" y1="11.5889" x2="2.57161" y2="14.6471" gradientUnits="userSpaceOnUse">
<linearGradient fileName="paint0_linear_112102_1824" x1="5.62978" y1="11.5889" x2="2.57161" y2="14.6471" gradientUnits="userSpaceOnUse">
<stop stop-color="#023B95"/>
<stop offset="0.9637" stop-color="#096CD9" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_112102_1824" x1="23.2722" y1="11.6281" x2="25.7451" y2="14.4382" gradientUnits="userSpaceOnUse">
<linearGradient fileName="paint1_linear_112102_1824" x1="23.2722" y1="11.6281" x2="25.7451" y2="14.4382" gradientUnits="userSpaceOnUse">
<stop stop-color="#023B95"/>
<stop offset="1" stop-color="#096DD9" stop-opacity="0"/>
</linearGradient>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -5,11 +5,11 @@
<path d="M7.75805 13.5498L7.76046 13.5522L9.93528 15.7276L18.3125 24.1043L28.8669 13.5498H7.75805Z" fill="#1890FF"/>
<path d="M47.7375 25.5V9.594H51.5655L55.7345 21.749L59.9475 9.594H63.7535V25.5H61.0365V13.026L56.8455 25.5H54.6125L50.4545 13.103V25.5H47.7375ZM70.3836 25.72C69.6576 25.72 69.0049 25.588 68.4256 25.324C67.8536 25.0527 67.4026 24.6677 67.0726 24.169C66.7499 23.663 66.5886 23.0507 66.5886 22.332C66.5886 21.078 67.0396 20.132 67.9416 19.494C68.8509 18.856 70.2992 18.5223 72.2866 18.493L73.9806 18.449V17.679C73.9806 17.0777 73.8009 16.6083 73.4416 16.271C73.0896 15.9263 72.5396 15.7577 71.7916 15.765C71.2416 15.7723 70.7356 15.9043 70.2736 16.161C69.8189 16.4103 69.5182 16.832 69.3716 17.426H67.0946C67.1386 16.6487 67.3622 15.9997 67.7656 15.479C68.1762 14.951 68.7372 14.5587 69.4486 14.302C70.1599 14.038 70.9812 13.906 71.9126 13.906C72.9979 13.906 73.8816 14.049 74.5636 14.335C75.2456 14.621 75.7479 15.0353 76.0706 15.578C76.3932 16.1133 76.5546 16.7623 76.5546 17.525V25.5H74.2996L74.0906 23.454C73.6799 24.312 73.1592 24.906 72.5286 25.236C71.9052 25.5587 71.1902 25.72 70.3836 25.72ZM71.2526 23.905C71.5826 23.905 71.9089 23.8463 72.2316 23.729C72.5542 23.6043 72.8476 23.4393 73.1116 23.234C73.3756 23.0213 73.5846 22.7867 73.7386 22.53C73.8999 22.2733 73.9806 22.0057 73.9806 21.727V19.934L72.5836 19.956C71.8942 19.9633 71.2929 20.0367 70.7796 20.176C70.2662 20.3153 69.8629 20.539 69.5696 20.847C69.2836 21.1477 69.1406 21.551 69.1406 22.057C69.1406 22.6363 69.3422 23.091 69.7456 23.421C70.1489 23.7437 70.6512 23.905 71.2526 23.905ZM79.686 25.5V14.126H82.304V15.754C82.48 15.4607 82.7183 15.1783 83.019 14.907C83.327 14.6357 83.7046 14.4157 84.152 14.247C84.5993 14.0783 85.1236 13.994 85.725 13.994C86.429 13.994 87.0816 14.1333 87.683 14.412C88.2916 14.6907 88.7793 15.127 89.146 15.721C89.52 16.315 89.707 17.0813 89.707 18.02V25.5H87.023V18.339C87.023 17.591 86.8176 17.0373 86.407 16.678C85.9963 16.3113 85.4793 16.128 84.856 16.128C84.4306 16.128 84.0273 16.2013 83.646 16.348C83.2646 16.4873 82.953 16.7037 82.711 16.997C82.4763 17.283 82.359 17.6423 82.359 18.075V25.5H79.686ZM98.4645 16.018H95.9235L95.9345 22.541C95.9345 22.8783 95.9712 23.1277 96.0445 23.289C96.1252 23.443 96.2498 23.5457 96.4185 23.597C96.5945 23.641 96.8292 23.663 97.1225 23.663H98.5195V25.357C98.3728 25.4157 98.1492 25.467 97.8485 25.511C97.5552 25.5623 97.1592 25.588 96.6605 25.588C95.7585 25.588 95.0582 25.4707 94.5595 25.236C94.0682 24.994 93.7272 24.653 93.5365 24.213C93.3458 23.773 93.2505 23.2487 93.2505 22.64V16.018H91.4025V14.126H93.3275L93.9985 10.749H95.9235V14.115H98.4645V16.018ZM103.845 14.126V25.5H101.271V14.126H103.845ZM103.878 9.605V12.135H101.227V9.605H103.878ZM111.358 25.72C110.566 25.72 109.814 25.5953 109.103 25.346C108.399 25.0893 107.805 24.6897 107.321 24.147C106.844 23.6043 106.543 22.904 106.419 22.046H108.773C108.875 22.464 109.055 22.805 109.312 23.069C109.576 23.333 109.887 23.5273 110.247 23.652C110.606 23.7693 110.973 23.828 111.347 23.828C112.029 23.828 112.579 23.7107 112.997 23.476C113.422 23.2413 113.635 22.8783 113.635 22.387C113.635 22.0277 113.517 21.7417 113.283 21.529C113.048 21.3163 112.674 21.155 112.161 21.045L109.994 20.55C109.033 20.3373 108.259 19.9853 107.673 19.494C107.093 19.0027 106.8 18.3097 106.793 17.415C106.785 16.7403 106.954 16.139 107.299 15.611C107.643 15.083 108.157 14.6687 108.839 14.368C109.521 14.06 110.364 13.906 111.369 13.906C112.696 13.906 113.759 14.203 114.559 14.797C115.358 15.3837 115.769 16.2233 115.791 17.316H113.514C113.433 16.8247 113.202 16.4433 112.821 16.172C112.439 15.8933 111.944 15.754 111.336 15.754C110.705 15.754 110.188 15.875 109.785 16.117C109.381 16.359 109.18 16.7293 109.18 17.228C109.18 17.5727 109.334 17.844 109.642 18.042C109.95 18.24 110.412 18.4087 111.028 18.548L113.074 19.043C113.66 19.1897 114.141 19.3877 114.515 19.637C114.889 19.8863 115.182 20.1613 115.395 20.462C115.607 20.7553 115.754 21.0597 115.835 21.375C115.923 21.683 115.967 21.969 115.967 22.233C115.967 22.9663 115.776 23.5933 115.395 24.114C115.021 24.6273 114.489 25.0233 113.8 25.302C113.11 25.5807 112.296 25.72 111.358 25.72Z" fill="#F0F0F0"/>
<defs>
<linearGradient id="paint0_linear_113047_293987" x1="9.43788" y1="14.0889" x2="6.37971" y2="17.1471" gradientUnits="userSpaceOnUse">
<linearGradient fileName="paint0_linear_113047_293987" x1="9.43788" y1="14.0889" x2="6.37971" y2="17.1471" gradientUnits="userSpaceOnUse">
<stop stop-color="#023B95"/>
<stop offset="0.9637" stop-color="#096CD9" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_113047_293987" x1="27.08" y1="14.1281" x2="29.5529" y2="16.9382" gradientUnits="userSpaceOnUse">
<linearGradient fileName="paint1_linear_113047_293987" x1="27.08" y1="14.1281" x2="29.5529" y2="16.9382" gradientUnits="userSpaceOnUse">
<stop stop-color="#023B95"/>
<stop offset="1" stop-color="#096DD9" stop-opacity="0"/>
</linearGradient>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

+3 -3
View File
@@ -10,7 +10,7 @@
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-E8484J3HQW"></script>
<script async src="https://www.googletagmanager.com/gtag/js?fileName=G-E8484J3HQW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
@@ -23,12 +23,12 @@
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
'https://www.googletagmanager.com/gtm.js?fileName='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-MD79JQ92');
</script>
<!-- End Google Tag Manager -->
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-MD79JQ92"
<noscript><iframe src="https://www.googletagmanager.com/ns.html?fileName=GTM-MD79JQ92"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<body class="mat-typography">
@@ -2,7 +2,7 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<font id="tabler-icons" horiz-adv-x="1010">
<font fileName="tabler-icons" horiz-adv-x="1010">
<font-face font-family="tabler-icons"
units-per-em="1000" ascent="986.5"
descent="100" />
@@ -466,7 +466,7 @@
<glyph glyph-name="eye"
unicode="&#xEA9A;"
horiz-adv-x="1010" d="M516 733C556.773 733 560.77 729.528 589 726C632.516 720.56 670.156 704.922 704 688L726 677C738.2 670.9 740.525 666.7 754 659C769.922 649.902 781.5360000000001 639.771 795 629C800 625 812 615 820 607L849 578C857 570 868 556 875 548C893.285 527.102 912.349 501.596 928 477C940.207 457.817 955.153 435.693 964 418C968 411 968 411 968 400C968 379.648 959.557 375.203 950 358C928.251 318.852 904.139 285.3 875 252C868 244 857 230 849 222L820 193C802.287 175.287 795.51 172.592 778 158C742.41 128.34 696.39 106.7720000000001 649 89C623.041 79.266 591.845 73.264 562 69C555 68 537 67 520 67C487.33 67 473.333 67.111 450 71C399.397 79.434 354.426 92.787 316 112C308 116 298 121 294 124S279 132 272 137C229.847 167.11 190.095 198.223 156 238C132.405 265.5270000000001 112.575 290.668 92 323C79.793 342.183 64.847 364.307 56 382C52 389 52 389 52 400S52 411 56 418C64.795 435.59 79.83 457.876 92 477C115.985 514.692 143.22 546.623 172 579C191.538 600.98 214.364 620.773 236 637C240 640 246 646 251 649S265 658 272 663S290 673 294 676C311.1840000000001 688.888 338.92 700.691 361 707C393.443 716.27 430.058 726.5799999999999 468 732C477 733 493 733 516 733zM516 649H484C467.648 649 457.45 645.207 442 643C395.6 636.371 359.993 615.496 325 598C321 596 314 590 308 586C263.323 556.2149999999999 228.148 517.006 193 476C173.717 453.503 157.908 423.862 142 400C153.634 382.55 171.86 350.925 187 332C205.758 308.553 221.375 288.625 242 268C276.23 233.77 310.108 208.446 355 186C400.804 163.098 452.058 150 521 150C560.515 150 590.894 159.112 622 168C639.276 172.936 654.538 182.3730000000001 670 189C687.648 196.563 697.034 204.023 712 214C737.046 230.697 755.885 245.885 778 268C786 276 798 291 805 299S825 322 833 332C847.43 350.0370000000001 860.85 375.8 873 392L878 400C844.156 450.766 807.083 507.552 762 547C700.717 600.6220000000001 630.204 649 516 649zM509 525C558.694 525 589.4110000000001 501.785 611 473C625.004 454.327 634 433.256 634 400C634 349.246 611.527 320.145 582 298C563.678 284.2580000000001 542.468 276 510 276C450.204 276 417.088 306.824 397 347C389.644 361.7140000000001 385 378.4400000000001 385 400C385 457.205 418.5470000000001 493.773 457 513C471.15 520.075 488.697 525 509 525zM513 441H504C490.663 441 475.34 428.019 472 418C463.103 391.308 476.182 359 504 359H516C533.581 359 551 376.93 551 394V406C551 415.938 545.763 422.237 540 428C533.334 434.666 524.893 441 513 441z" />
<glyph glyph-name="face-id"
<glyph glyph-name="face-fileName"
unicode="&#xEA9B;"
horiz-adv-x="1010" d="M218 556C209.645 539.29 199.08 525 174 525C153.3 525 143.059 541.8820000000001 136 556L135 606C135 682.059 137.17 703.1700000000001 172 738C190.965 756.965 209.457 765.614 239 773C245 774 259 775 300 775L354 774C368.407 766.797 385 756.996 385 736C385 710.9200000000001 370.71 700.355 354 692H304C249.52 692 254.991 693.996 239 686C230.582 681.7909999999999 224.127 670.2529999999999 220 662C219 659 218 643 218 606V556zM666 692C649.29 700.355 635 710.9200000000001 635 736C635 752.796 646.732 763.155 657 770C669.04 778.026 680.096 775 720 775C760 775 773 775 780 774C836.017 762.797 869.418 726.327 883 672C884 667 884 653 884 611V556C876.848 541.696 866.898 525 846 525C819.073 525 812.852 538.722 802 555V606C802 660.48 803.996 655.009 796 671C791.791 679.418 780.253 685.873 772 690C769 691 753 692 716 692H666zM427 484C427 458.255 409.5710000000001 442 383 442C359.884 442 348.762 457.953 344 477C338.442 504.788 359.403 525 387 525C403.887 525 411.81 514.19 420 506C424.2080000000001 501.792 427 491.67 427 484zM677 484C677 458.255 659.571 442 633 442C609.884 442 598.762 457.953 594 477C588.442 504.788 609.403 525 637 525C653.887 525 661.81 514.19 670 506C674.208 501.792 677 491.67 677 484zM364 272C364 298.057 380.33 317 406 317C422.185 317 427.282 310.574 438 302C457.112 286.7100000000001 475.849 273 510 273C535.0360000000001 273 553.982 280.9880000000001 569 291C583.76 300.8400000000001 595.292 321.1420000000001 621 316C641.894 310.776 656 298.1 656 272C656 247.374 632.073 233.716 616 223C591.934 206.9550000000001 561.076 190 523 190H497C460.486 190 429.795 206.7240000000001 406 221C401 224 393 230 388 235C376.798 246.202 364 252.692 364 272zM354 108C370.71 99.645 385 89.08 385 64C385 43.72 368.607 32.26 354 26H299C257 26 243 26 238 27C194.835 37.7910000000001 161.481 60.297 146 99C132.69 132.274 135 133.653 135 194L136 244C138.887 249.774 141.578 257.578 146 262C153.18 269.18 163.715 275 177 275C199.755 275 210.371 259.259 218 244V194C218 139.52 216.004 144.991 224 129C228.209 120.582 239.747 114.127 248 110C251 109 267 108 304 108H354zM845 275C818.693 275 812.673 261.009 802 245V194C802 139.52 803.996 144.991 796 129C791.791 120.582 780.253 114.127 772 110C769 109 753 108 716 108H666C649.06 99.53 635 89.4 635 64C635 43.72 651.393 32.26 666 26H721C763 26 777 26 782 27C836.996 40.749 869.1659999999999 72.665 883 128C884 133 884 147 884 189V244C876.493 259.015 867.266 275 845 275z" />
<glyph glyph-name="file-check"
@@ -580,7 +580,7 @@
<glyph glyph-name="ice-cream"
unicode="&#xEAC2;"
horiz-adv-x="1010" d="M508 817C603.88 817 664.65 764.698 697 700C707.161 679.678 712.643 658.143 717 632C718 624 718 579 718 404C718 194 719 184 717 179C712.907 166.723 701.956 155.24 689 152C683 150 679 150 617 150H552V76C552 25 552 0 551 -4C547.875 -13.374 544.577 -19.423 538 -26C535 -29 531 -33 528 -34C516.328 -37.89 504.213 -38.071 492 -34C480.802 -30.268 472.794 -15.3819999999999 469 -4C468 0 468 25 468 76V150H403C341 150 337 150 331 152C317.723 155.3200000000001 307.1600000000001 166.52 303 179C301 184 302 194 302 404C302 610.461 299.339 608.03 307 654C318.09 720.543 363.088 762.652 412 792C429.372 802.423 450.2440000000001 808.45 473 813C478 814 485 816 490 816S503 817 508 817zM509 733C462.205 733 430.481 710.309 410 683C398.012 667.016 390.687 645.435 386 622C385 617 385 595 385 570C385 546 386 527 386 527S441 551 509 581L632 634L631 637C631 652.25 617.654 672.794 610 683C589.303 710.596 556.602 733 509 733zM634 544S578 520 510 490L386 435L385 398V360S442 384 510 414L634 469L635 506C635 527 634 544 634 544zM634 377C633 377 577 353 509 323L385 268V234H635V306C635 363 635 377 634 377z" />
<glyph glyph-name="id"
<glyph glyph-name="fileName"
unicode="&#xEAC3;"
horiz-adv-x="1010" d="M514 775L782 774L792 772C842.074 761.985 874.017 734.645 900 700C903 696 906 687 909 682C918.61 665.984 921.843 645.7860000000001 926 625C927 618 927 545 927 400S927 182 926 175C921.858 154.292 918.703 134.173 909 118C906 113 903 104 900 100C891.886 89.182 881.96 75.769 871 67C848.77 49.216 825.517 33.586 792 28L782 26H238L228 28C178.156 36.308 145.362 66.184 120 100C117 104 114 113 111 118C101.39 134.016 98.157 154.214 94 175C93 182 93 255 93 400S93 618 94 625C98.142 645.708 101.297 665.827 111 682C114 687 117 696 120 700C142.08 729.44 169.144 756.7860000000001 210 767C218.799 769.2 229.507 774 239 774C243 774 366 775 514 775zM512 691C258 691 247 691 240 689C206.356 680.5889999999999 187.393 656.874 178 624L177 404C177 205 178 183 179 176C185.187 151.254 197.132 130.435 218 120C222 118 229 115 234 113L244 110H776L786 113C812.636 123.654 833.53 141.1230000000001 841 171C843 177 842 187 842 400C842 611 843 623 841 629C833.992 657.03 811.042 676.983 786 687L776 690zM392 608C417.727 608 438.0660000000001 597.1600000000001 455 587C474.795 575.123 487.196 555.674 499 536C502 531 504 520 506 514C508 505 510 499 510 488C510 426.175 481.117 395.4700000000001 442 372C425.3 361.98 411.213 359 385 359C351.032 359 329.3450000000001 367.524 310 383C283.1600000000001 404.472 260 436.325 260 484C260 514.8140000000001 270.304 534.5070000000001 282 554C292.468 571.446 310.44 587.1759999999999 330 595C344.477 600.7909999999999 356.6 608 376 608H392zM676 609C711.333 609 719.783 610.405 736 605C749.854 600.3820000000001 760 584.4100000000001 760 566C760 545.165 746.826 532.275 731 527C725 525 724 525 675 525L624 526C617.798 529.1 610.708 532.2919999999999 606 537C591.77 551.23 589.3 580.3 605 596C609.832 600.832 619.339 608 628 608C631 608 653 609 676 609zM385 442C409.082 442 426 459.774 426 484C426 507.836 407.284 525 384 525C362.03 525 344 506.455 344 484C344 459.759 360.938 442 385 442zM677 442C716 442 724 441 729 440C745.644 434.452 760 422.367 760 400C760 376.822 746.398 365.8 729 360C724 359 714 358 677 358C623.792 358 616.509 354.237 598 382C583.455 403.818 603.045 428.363 616 437C619 439 624 441 626 441S652 442 677 442zM510 275C296 275 295 275 289 273C273.23 267.7430000000001 260 254.653 260 234C260 212.089 273.367 198.544 290 193C295 191 309 192 510 192S725 191 730 193C746.654 198.552 760 212.35 760 234C760 254.8350000000001 746.826 267.725 731 273C725 275 725 275 510 275z" />
<glyph glyph-name="inbox"

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.3 MiB

+1 -1
View File
@@ -7,7 +7,7 @@
FROM openjdk:24
WORKDIR /jambotron/
VOLUME /jambotron_data/uploads
#VOLUME /jambotron_data/uploads
COPY './build/libs/jambotron-0.0.1-SNAPSHOT.jar' '/app/jambotron.jar'
#COPY --from=BUILD_IMAGE /jambotron/build/libs/jambotron-0.0.1-SNAPSHOT.jar .
#EXPOSE 8080
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: 'jambotron-container'
build:
context: ../../
dockerfile: ./src/Docker/Dockerfile
dockerfile: ./src/DevOps/Dockerfile
ports:
# - "8081:80"
- "8443:443"
@@ -1,11 +1,14 @@
package com.jambotronGroup.jambotron.controllers;
import com.jambotronGroup.jambotron.fileUpload.Base64ImageResponse;
import com.jambotronGroup.jambotron.fileUpload.FilesStorageService;
import com.jambotronGroup.jambotron.fileUpload.ResponseImageUploadResult;
import com.jambotronGroup.jambotron.fileUpload.ResponseMessage;
import com.jambotronGroup.jambotron.model.FileInfo;
import com.jambotronGroup.jambotron.model.User;
import com.jambotronGroup.jambotron.payload.request.Base64ImageRequest;
import com.jambotronGroup.jambotron.payload.request.SaveZhipuAiImageRequest;
import com.jambotronGroup.jambotron.payload.request.UploadImageResponse;
import com.jambotronGroup.jambotron.security.AuthenticationFacade;
import com.jambotronGroup.jambotron.system.SystemServiceImpl;
import com.jambotronGroup.jambotron.utils.FilesRoutingHelper;
@@ -16,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Controller;
@@ -28,7 +32,9 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Controller
@@ -129,7 +135,6 @@ public class FilesController {
return ResponseEntity.status(HttpStatus.OK).body(fileInfos);
}
@GetMapping("/api/file/files")
public ResponseEntity<List<FileInfo>> getListFiles() {
List<FileInfo> fileInfos = storageService.loadAll().map(path -> {
@@ -157,4 +162,48 @@ public class FilesController {
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"").body(file);
}
@PostMapping("/api/user/user-images/base64-image")
public ResponseEntity<Base64ImageResponse> getUserBase64Image(@RequestBody String imageUrl) {
User user = authenticationFacade.getUser();
String message = "";
try {
String filename = storageService.getFileNameFromUrl(imageUrl);
Base64ImageResponse base64Image = storageService.getUserImageAsBase64(user.getId().toString(),filename);
return ResponseEntity.ok(base64Image);
} catch (RuntimeException e) {
logger.error("Error retrieving base64 image for user {}: {}", user.getId(), e.getMessage());
message = "Error retrieving base64 image for fileUrl" + imageUrl + ". \tError: " + e.getMessage();
return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new Base64ImageResponse(message));
}
}
@PostMapping("/api/user/user-images/upload-base64-image")
public ResponseEntity<UploadImageResponse> uploadBase64Image(@RequestBody Base64ImageRequest base64ImageRequest) {
if (base64ImageRequest.getMime() == null || base64ImageRequest.getData() == null) {
return ResponseEntity.badRequest().build();
}
User user = authenticationFacade.getUser();
try {
String fullFileName = storageService.saveUserBase64Image(user.getId().toString(),base64ImageRequest.getData(), base64ImageRequest.getMime());
String url = FilesRoutingHelper.getUserImageUrl(fullFileName);
return ResponseEntity.status(HttpStatus.OK).body(new UploadImageResponse(
storageService.getFileNameFromUrl(url),
url,
base64ImageRequest.getMime(),
base64ImageRequest.getData().length()
));
} catch (Exception e) {
logger.error("Error uploading base64 image for user {}: {}", user.getId(), e.getMessage());
String message = "Error uploading base64 image. Error: " + e.getMessage();
return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new UploadImageResponse(message));
}
}
}
@@ -177,6 +177,7 @@ public class TutorialController {
servTutorial.setDescription(tutorial.getDescription());
servTutorial.setPublished(tutorial.isPublished());
servTutorial.setTobepublished(tutorial.isTobepublished());
servTutorial.setBody(tutorial.getBody());
try {
@@ -0,0 +1,32 @@
package com.jambotronGroup.jambotron.fileUpload;
public class Base64ImageResponse extends ResponseMessage {
private String mime;
private String data;
public Base64ImageResponse(String message) {
super(message);
}
public Base64ImageResponse(String mime, String data) {
super("Image data retrieved successfully");
this.mime = mime;
this.data = data;
}
public String getMime() {
return mime;
}
public void setMime(String mime) {
this.mime = mime;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
@@ -1,5 +1,6 @@
package com.jambotronGroup.jambotron.fileUpload;
import com.jambotronGroup.jambotron.payload.request.UploadImageResponse;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
@@ -14,12 +15,16 @@ public interface FilesStorageService {
public String save(String userID,MultipartFile file);
public String saveUserBase64Image(String userID, String base64Image, String mimeType);
public void save(MultipartFile file);
public Resource load(String filename);
public Resource loadUserImage(String userID, String filename);
public Base64ImageResponse getUserImageAsBase64(String userID, String filename);
public void deleteAll();
public Stream<Path> loadAll();
@@ -2,6 +2,7 @@ package com.jambotronGroup.jambotron.fileUpload;
import com.jambotronGroup.jambotron.controllers.AuthController;
import com.jambotronGroup.jambotron.payload.request.UploadImageResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
@@ -17,6 +18,7 @@ import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Stream;
@Service
@@ -24,6 +26,17 @@ public class FilesStorageServiceImpl implements FilesStorageService {
private static final Logger _logger = LoggerFactory.getLogger(AuthController.class);
private static final Set<String> ALLOWED_MIME = Set.of(
"image/png", "image/jpeg", "image/webp", "image/gif"
);
private static final Map<String, String> EXT_BY_MIME = Map.of(
"image/png", "png",
"image/jpeg", "jpg",
"image/webp", "webp",
"image/gif", "gif"
);
// Update paths to use the Docker volume
private final Path root = Paths.get("/jambotron_data/uploads/user-images/");
@@ -54,6 +67,29 @@ public class FilesStorageServiceImpl implements FilesStorageService {
}
}
@Override
public Base64ImageResponse getUserImageAsBase64(String userID,String filename){
Path userPath = this.root.resolve(userID);
Path file = userPath.resolve(filename);
try {
byte[] bytes = Files.readAllBytes(file);
String mime = Files.probeContentType(file);
if (mime == null || mime.isEmpty()) {
mime = "application/octet-stream"; // Default MIME type if not found
}
String base64 = Base64.getEncoder().encodeToString(bytes);
return new Base64ImageResponse(mime,base64);
}catch (Exception e) {
_logger.error(" getUserImageAsBase64 Error loading user image: " + filename, e);
throw new RuntimeException("getUserImageAsBase64 Could not load user image: " + filename);
}
}
/**
* Moves a file from a user's directory to the public directory with a new name.
* StandardCopyOption.REPLACE_EXISTING
@@ -192,4 +228,70 @@ public class FilesStorageServiceImpl implements FilesStorageService {
throw new RuntimeException("Could not load the files!");
}
}
@Override
public String saveUserBase64Image(String userID, String base64Image, String mimeType) {
if (!ALLOWED_MIME.contains(mimeType)) {
_logger.error("Unsupported MIME type: " + mimeType);
throw new RuntimeException("Unsupported MIME type: " + mimeType);
}
String ext = EXT_BY_MIME.get(mimeType);
if (ext == null) {
throw new RuntimeException("No extension found for MIME type: " + mimeType);
}
byte[] bytes;
try {
bytes = Base64.getDecoder().decode(base64Image);
} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid Base64 data: " + e.getMessage());
}
if (!looksLikeMime(mimeType,bytes)) {
throw new RuntimeException("Base64 data does not match the expected MIME type: " + mimeType);
}
String id = UUID.randomUUID().toString().replace("-", "");
String safeBaseName = "user-image-" + userID;
String storedName = safeBaseName + "-" + id + "." + ext;
try {
Path userPath = this.root.resolve(userID);
userPath.toFile().mkdirs(); // Ensure user directory exists
Path targetPath = userPath.resolve(storedName);
Files.write(targetPath, bytes);
return targetPath.getFileName().toString();
} catch (IOException e) {
throw new RuntimeException("Could not save the image: " + e.getMessage());
}
}
// Very lightweight magic-number checks
private boolean looksLikeMime(String mime, byte[] b) {
if (b.length < 12) return false;
switch (mime) {
case "image/png":
return b[0]==(byte)0x89 && b[1]==0x50 && b[2]==0x4E && b[3]==0x47 && // 89 'P''N''G'
b[4]==0x0D && b[5]==0x0A && b[6]==0x1A && b[7]==0x0A;
case "image/jpeg":
return b[0]==(byte)0xFF && b[1]==(byte)0xD8 && b[b.length-2]==(byte)0xFF && b[b.length-1]==(byte)0xD9;
case "image/gif":
return b[0]==0x47 && b[1]==0x49 && b[2]==0x46 && b[3]==0x38; // "GIF8"
case "image/webp":
// "RIFF"...."WEBP"
return b[0]==0x52 && b[1]==0x49 && b[2]==0x46 && b[3]==0x46 &&
b[8]==0x57 && b[9]==0x45 && b[10]==0x42 && b[11]==0x50;
default:
return false;
}
}
}
@@ -0,0 +1,22 @@
package com.jambotronGroup.jambotron.payload.request;
public class Base64ImageRequest {
private String mime; // e.g., "image/png"
private String data; // base64 without "data:" prefix
public String getMime() {
return mime;
}
public void setMime(String mime) {
this.mime = mime;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
@@ -0,0 +1,47 @@
package com.jambotronGroup.jambotron.payload.request;
import com.jambotronGroup.jambotron.fileUpload.ResponseMessage;
public class UploadImageResponse extends ResponseMessage {
private String fileName;
private String url;
private String mime;
private long size;
public UploadImageResponse(String message) {
super(message);
}
public UploadImageResponse(String fileName, String url, String mime, long size) {
super(
String.format("Image uploaded successfully: %s, URL: %s, MIME: %s, Size: %d bytes", fileName, url, mime, size)
);
this.fileName = fileName;
this.url = url;
this.mime = mime;
this.size = size;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getMime() {
return mime;
}
public void setMime(String mime) {
this.mime = mime;
}
}
@@ -33,11 +33,11 @@ spring.ai.zhipuai.api-key = 628447c8c65845a48a7226391464a2ea.Dw8ci6TiW0BRF5LI
app.jwtSecret= ======================jambotron=back=================
app.jwtCookieName=jambotron-jwt-cookie
app.jwtRefreshCookieName= jambotron-jwt-cookie-refresh
#app.jwtExpirationMs= 800000
#app.jwtRefreshExpirationMs= 86400000
app.jwtExpirationMs= 800000
app.jwtRefreshExpirationMs= 86400000
app.jwtExpirationMs= 30000
app.jwtRefreshExpirationMs= 60000
#app.jwtExpirationMs= 30000
#app.jwtRefreshExpirationMs= 60000
#=============File Upload Configurations========================
spring.servlet.multipart.max-file-size=50MB