diff --git a/jambotron-ui/package-lock.json b/jambotron-ui/package-lock.json index fba3123..281227c 100644 --- a/jambotron-ui/package-lock.json +++ b/jambotron-ui/package-lock.json @@ -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", diff --git a/jambotron-ui/package.json b/jambotron-ui/package.json index 4e9a8bc..234fc42 100644 --- a/jambotron-ui/package.json +++ b/jambotron-ui/package.json @@ -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", diff --git a/jambotron-ui/public/jambotron_logo.svg b/jambotron-ui/public/jambotron_logo.svg index 9135899..e917217 100644 --- a/jambotron-ui/public/jambotron_logo.svg +++ b/jambotron-ui/public/jambotron_logo.svg @@ -4,6 +4,6 @@ Layer 1 jambotron + font-size="24" fileName="svg_1" y="27.49999" x="2.59999" stroke-width="0" stroke="#000" fill="#000000">jambotron diff --git a/jambotron-ui/src/app/app.config.ts b/jambotron-ui/src/app/app.config.ts index b527fc1..0f03633 100644 --- a/jambotron-ui/src/app/app.config.ts +++ b/jambotron-ui/src/app/app.config.ts @@ -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) ] }; diff --git a/jambotron-ui/src/app/app.ts b/jambotron-ui/src/app/app.ts index 571584f..095273c 100644 --- a/jambotron-ui/src/app/app.ts +++ b/jambotron-ui/src/app/app.ts @@ -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); + } + }); + }) } } diff --git a/jambotron-ui/src/app/components/file-upload.component/file-upload.component.html b/jambotron-ui/src/app/components/file-upload.component/file-upload.component.html index 18201d4..9f26f16 100644 --- a/jambotron-ui/src/app/components/file-upload.component/file-upload.component.html +++ b/jambotron-ui/src/app/components/file-upload.component/file-upload.component.html @@ -14,7 +14,7 @@ diff --git a/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.html b/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.html new file mode 100644 index 0000000..9e394a7 --- /dev/null +++ b/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.html @@ -0,0 +1,103 @@ + diff --git a/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.scss b/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.scss new file mode 100644 index 0000000..d93e233 --- /dev/null +++ b/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.scss @@ -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; +} diff --git a/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.spec.ts b/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.spec.ts new file mode 100644 index 0000000..1beb93d --- /dev/null +++ b/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PhotoEditComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PhotoEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.ts b/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.ts new file mode 100644 index 0000000..d7c8779 --- /dev/null +++ b/jambotron-ui/src/app/components/photo-edit.component/photo-edit.component.ts @@ -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(); +@Output() imageCroppedEvent = new EventEmitter(); +@Output() errorEvent = new EventEmitter(); + imageLoaded = false; + isProcessing = false; + + +@Input() set source(data: File | string | any) { + if (data instanceof Event) { + // @ts-ignore + const file = (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'; +*/ diff --git a/jambotron-ui/src/app/components/photo-edit.component/photo-editor.service.spec.ts b/jambotron-ui/src/app/components/photo-edit.component/photo-editor.service.spec.ts new file mode 100644 index 0000000..94f9a78 --- /dev/null +++ b/jambotron-ui/src/app/components/photo-edit.component/photo-editor.service.spec.ts @@ -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(); + }); +}); diff --git a/jambotron-ui/src/app/components/photo-edit.component/photo-editor.service.ts b/jambotron-ui/src/app/components/photo-edit.component/photo-editor.service.ts new file mode 100644 index 0000000..b524f0e --- /dev/null +++ b/jambotron-ui/src/app/components/photo-edit.component/photo-editor.service.ts @@ -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; + private photoEditorComponentRef!: ComponentRef; + + + constructor( + private componentFactoryResolver: ComponentFactoryResolver, + private appRef: ApplicationRef, + private injector: Injector) { + } + + open(source: Event | string | File | any, options?: Options): Observable { + const componentFactory = this.componentFactoryResolver.resolveComponentFactory(PhotoEditComponent); + const componentRef = componentFactory.create(this.injector); + this.appRef.attachView(componentRef.hostView); + const domElem = (componentRef.hostView as EmbeddedViewRef).rootNodes[0] as HTMLElement; + document.body.appendChild(domElem); + this.photoEditorSubscriber = new Subject(); + 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; +} +*/ diff --git a/jambotron-ui/src/app/components/spinner/spinner.component.html b/jambotron-ui/src/app/components/spinner/spinner.component.html index 7a4c0db..2b74025 100644 --- a/jambotron-ui/src/app/components/spinner/spinner.component.html +++ b/jambotron-ui/src/app/components/spinner/spinner.component.html @@ -1,5 +1,5 @@ @if (isSpinnerVisible) { -
+
@if (spinner() === Spinkit.skLine) {
diff --git a/jambotron-ui/src/app/helpers/auth.interceptor.ts b/jambotron-ui/src/app/helpers/auth.interceptor.ts index be4ed85..8c1d5cf 100644 --- a/jambotron-ui/src/app/helpers/auth.interceptor.ts +++ b/jambotron-ui/src/app/helpers/auth.interceptor.ts @@ -24,9 +24,9 @@ export class AuthInterceptor implements HttpInterceptor { intercept(req: HttpRequest, next: HttpHandler): Observable> { - // 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); diff --git a/jambotron-ui/src/app/models/file-info.ts b/jambotron-ui/src/app/models/file-info.ts index fc55c33..1cf4a15 100644 --- a/jambotron-ui/src/app/models/file-info.ts +++ b/jambotron-ui/src/app/models/file-info.ts @@ -1,4 +1,6 @@ export class FileInfo { name: string | undefined; url: string | undefined; + mode: string | undefined;//View or CropView + base64string: string | undefined; } diff --git a/jambotron-ui/src/app/models/role.ts b/jambotron-ui/src/app/models/role.ts index be13639..685371b 100644 --- a/jambotron-ui/src/app/models/role.ts +++ b/jambotron-ui/src/app/models/role.ts @@ -1,4 +1,4 @@ export class Role { - id?:any; + fileName?:any; name?:string; } diff --git a/jambotron-ui/src/app/models/setting.ts b/jambotron-ui/src/app/models/setting.ts index d26db99..840554a 100644 --- a/jambotron-ui/src/app/models/setting.ts +++ b/jambotron-ui/src/app/models/setting.ts @@ -1,5 +1,5 @@ export class Setting { - id?:any; + fileName?:any; name?:string; value?:boolean; } diff --git a/jambotron-ui/src/app/models/tutorial.model.ts b/jambotron-ui/src/app/models/tutorial.model.ts index e41afe1..a195d0c 100644 --- a/jambotron-ui/src/app/models/tutorial.model.ts +++ b/jambotron-ui/src/app/models/tutorial.model.ts @@ -1,6 +1,6 @@ export class Tutorial { isSelected?: boolean; - id?: any; + fileName?: any; title?: string; description?: string; published?: boolean; diff --git a/jambotron-ui/src/app/models/user.model.ts b/jambotron-ui/src/app/models/user.model.ts index aced2e3..4def107 100644 --- a/jambotron-ui/src/app/models/user.model.ts +++ b/jambotron-ui/src/app/models/user.model.ts @@ -1,5 +1,5 @@ export class User { - id?:any; + fileName?:any; username?:string; email?:string; password?:string; diff --git a/jambotron-ui/src/app/modules/admin-module/admin-module.service.ts b/jambotron-ui/src/app/modules/admin-module/admin-module.service.ts index ee2b26b..70c8954 100644 --- a/jambotron-ui/src/app/modules/admin-module/admin-module.service.ts +++ b/jambotron-ui/src/app/modules/admin-module/admin-module.service.ts @@ -14,7 +14,7 @@ export class AdminModuleService { } - updateUserRoles(id: any,data: User): Observable { - return this.http.put(`${this.baseUrl}/users/${id}`, data); + updateUserRoles(fileName: any,data: User): Observable { + return this.http.put(`${this.baseUrl}/users/${fileName}`, data); } } diff --git a/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.ts b/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.ts index bd93db9..47cd514 100644 --- a/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.ts +++ b/jambotron-ui/src/app/modules/admin-module/admin.component/admin.component.ts @@ -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); 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' - } - ] - } - ]; + } } diff --git a/jambotron-ui/src/app/modules/admin-module/admin.module.ts b/jambotron-ui/src/app/modules/admin-module/admin.module.ts index 105676b..1b9fc4e 100644 --- a/jambotron-ui/src/app/modules/admin-module/admin.module.ts +++ b/jambotron-ui/src/app/modules/admin-module/admin.module.ts @@ -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] }) diff --git a/jambotron-ui/src/app/modules/admin-module/users.component/users.component.ts b/jambotron-ui/src/app/modules/admin-module/users.component/users.component.ts index b7ebcb4..fa747c8 100644 --- a/jambotron-ui/src/app/modules/admin-module/users.component/users.component.ts +++ b/jambotron-ui/src/app/modules/admin-module/users.component/users.component.ts @@ -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); } diff --git a/jambotron-ui/src/app/modules/ai-module/generate-image.component/generate-image.component.html b/jambotron-ui/src/app/modules/ai-module/generate-image.component/generate-image.component.html index 1fc5881..babd5c0 100644 --- a/jambotron-ui/src/app/modules/ai-module/generate-image.component/generate-image.component.html +++ b/jambotron-ui/src/app/modules/ai-module/generate-image.component/generate-image.component.html @@ -29,9 +29,9 @@ - + @if (isLoggedIn) { diff --git a/jambotron-ui/src/app/modules/moderator-module/tutorials-list.component/tutorials-list.component.ts b/jambotron-ui/src/app/modules/moderator-module/tutorials-list.component/tutorials-list.component.ts index 9c51eb1..27dcfd0 100644 --- a/jambotron-ui/src/app/modules/moderator-module/tutorials-list.component/tutorials-list.component.ts +++ b/jambotron-ui/src/app/modules/moderator-module/tutorials-list.component/tutorials-list.component.ts @@ -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); }, diff --git a/jambotron-ui/src/app/modules/tutorials-module/tutorial-view.component/tutorial-view.component.html b/jambotron-ui/src/app/modules/tutorials-module/tutorial-view.component/tutorial-view.component.html index f6a9b13..809e2b9 100644 --- a/jambotron-ui/src/app/modules/tutorials-module/tutorial-view.component/tutorial-view.component.html +++ b/jambotron-ui/src/app/modules/tutorials-module/tutorial-view.component/tutorial-view.component.html @@ -1,5 +1,5 @@
- +
diff --git a/jambotron-ui/src/app/modules/tutorials-module/tutorial-view.component/tutorial-view.component.ts b/jambotron-ui/src/app/modules/tutorials-module/tutorial-view.component/tutorial-view.component.ts index bea5513..c094a65 100644 --- a/jambotron-ui/src/app/modules/tutorials-module/tutorial-view.component/tutorial-view.component.ts +++ b/jambotron-ui/src/app/modules/tutorials-module/tutorial-view.component/tutorial-view.component.ts @@ -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; } diff --git a/jambotron-ui/src/app/modules/tutorials-module/tutorials-api.service.ts b/jambotron-ui/src/app/modules/tutorials-module/tutorials-api.service.ts index 1294704..fd1f833 100644 --- a/jambotron-ui/src/app/modules/tutorials-module/tutorials-api.service.ts +++ b/jambotron-ui/src/app/modules/tutorials-module/tutorials-api.service.ts @@ -17,6 +17,6 @@ export class TutorialsApiService { return this.http.get(`${this.baseUrl}/tutorials`); } - getTutorial(id: string | null | undefined): Observable { - return this.http.get(`${this.baseUrl}/tutorial-get/${id}`); + getTutorial(fileName: string | null | undefined): Observable { + return this.http.get(`${this.baseUrl}/tutorial-get/${fileName}`); }} diff --git a/jambotron-ui/src/app/modules/tutorials-module/tutorials-card-view.component/tutorials-card-view.component.html b/jambotron-ui/src/app/modules/tutorials-module/tutorials-card-view.component/tutorials-card-view.component.html index 555bb13..b169cf0 100644 --- a/jambotron-ui/src/app/modules/tutorials-module/tutorials-card-view.component/tutorials-card-view.component.html +++ b/jambotron-ui/src/app/modules/tutorials-module/tutorials-card-view.component/tutorials-card-view.component.html @@ -4,7 +4,7 @@ Photo of a - + diff --git a/jambotron-ui/src/app/modules/tutorials-module/tutorials-card-view.component/tutorials-card-view.component.scss b/jambotron-ui/src/app/modules/tutorials-module/tutorials-card-view.component/tutorials-card-view.component.scss index 7929a90..5cd8652 100644 --- a/jambotron-ui/src/app/modules/tutorials-module/tutorials-card-view.component/tutorials-card-view.component.scss +++ b/jambotron-ui/src/app/modules/tutorials-module/tutorials-card-view.component/tutorials-card-view.component.scss @@ -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; + } diff --git a/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.html b/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.html new file mode 100644 index 0000000..f20ad90 --- /dev/null +++ b/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.html @@ -0,0 +1,19 @@ +

dialog-cropp-image.component works!

+ + + + + + diff --git a/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.scss b/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.spec.ts b/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.spec.ts new file mode 100644 index 0000000..a9aa4fb --- /dev/null +++ b/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DialogCroppImageComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DialogCroppImageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.ts b/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.ts new file mode 100644 index 0000000..5a488b4 --- /dev/null +++ b/jambotron-ui/src/app/modules/user-module/dialog-cropp-image.component/dialog-cropp-image.component.ts @@ -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; + }) + } + } + + +} diff --git a/jambotron-ui/src/app/modules/user-module/images.component/images.component.html b/jambotron-ui/src/app/modules/user-module/images.component/images.component.html index 1f1aeef..3dc027c 100644 --- a/jambotron-ui/src/app/modules/user-module/images.component/images.component.html +++ b/jambotron-ui/src/app/modules/user-module/images.component/images.component.html @@ -1,25 +1,122 @@ -

images.component works!

-
- - @for (fileInfo of fileInfos; track fileInfo) {
- - - - - - + @if(fileInfo.mode !=="CropView"){ + + }@else{ + - - -
+ + + } + + @if(fileInfo.mode !=="CropView"){ + + + + + + + + + }@else{ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ + + + + + + + + + + + + Crop 716x400 + + + + + + + +
+ } +
diff --git a/jambotron-ui/src/app/modules/user-module/images.component/images.component.scss b/jambotron-ui/src/app/modules/user-module/images.component/images.component.scss index 0928c5a..b01c6f5 100644 --- a/jambotron-ui/src/app/modules/user-module/images.component/images.component.scss +++ b/jambotron-ui/src/app/modules/user-module/images.component/images.component.scss @@ -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; +} + diff --git a/jambotron-ui/src/app/modules/user-module/images.component/images.component.ts b/jambotron-ui/src/app/modules/user-module/images.component/images.component.ts index 5d63db3..0f3c50e 100644 --- a/jambotron-ui/src/app/modules/user-module/images.component/images.component.ts +++ b/jambotron-ui/src/app/modules/user-module/images.component/images.component.ts @@ -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"); + } + } } diff --git a/jambotron-ui/src/app/modules/user-module/tutorial-edit.component/tutorial-edit.component.html b/jambotron-ui/src/app/modules/user-module/tutorial-edit.component/tutorial-edit.component.html index fbd0be4..33f001b 100644 --- a/jambotron-ui/src/app/modules/user-module/tutorial-edit.component/tutorial-edit.component.html +++ b/jambotron-ui/src/app/modules/user-module/tutorial-edit.component/tutorial-edit.component.html @@ -70,7 +70,7 @@ - + diff --git a/jambotron-ui/src/app/modules/user-module/tutorial-edit.component/tutorial-edit.component.ts b/jambotron-ui/src/app/modules/user-module/tutorial-edit.component/tutorial-edit.component.ts index 0872c20..7d9bfe4 100644 --- a/jambotron-ui/src/app/modules/user-module/tutorial-edit.component/tutorial-edit.component.ts +++ b/jambotron-ui/src/app/modules/user-module/tutorial-edit.component/tutorial-edit.component.ts @@ -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); diff --git a/jambotron-ui/src/app/modules/user-module/tutorials-list.component/tutorials-list.component.html b/jambotron-ui/src/app/modules/user-module/tutorials-list.component/tutorials-list.component.html index fb527b5..5681b81 100644 --- a/jambotron-ui/src/app/modules/user-module/tutorials-list.component/tutorials-list.component.html +++ b/jambotron-ui/src/app/modules/user-module/tutorials-list.component/tutorials-list.component.html @@ -37,13 +37,13 @@ } @case('isEdit') {
- @@ -98,7 +98,7 @@ @@ -116,7 +116,7 @@ diff --git a/jambotron-ui/src/app/modules/user-module/tutorials-list.component/tutorials-list.component.ts b/jambotron-ui/src/app/modules/user-module/tutorials-list.component/tutorials-list.component.ts index 37e4ed5..cdc480b 100644 --- a/jambotron-ui/src/app/modules/user-module/tutorials-list.component/tutorials-list.component.ts +++ b/jambotron-ui/src/app/modules/user-module/tutorials-list.component/tutorials-list.component.ts @@ -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); }, diff --git a/jambotron-ui/src/app/modules/user-module/user-api.service.ts b/jambotron-ui/src/app/modules/user-module/user-api.service.ts index 8dfda62..55b711a 100644 --- a/jambotron-ui/src/app/modules/user-module/user-api.service.ts +++ b/jambotron-ui/src/app/modules/user-module/user-api.service.ts @@ -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 { + return this.http.post(`${this.baseUrl}/user-images/upload-base64-image`, base64Image, { headers: { "Content-Type": "application/json" } } + ); + } + upload(file: File): Observable> { const formData: FormData = new FormData(); formData.append('file', file); @@ -36,32 +42,59 @@ export class UserApiService { return this.http.get(`${this.baseUrl}/getImages`); } + + loadImageAsBase64(imageUrl: string): Observable { + return this.http.post(`${this.baseUrl}/user-images/base64-image`, imageUrl) + .pipe(map(r => `data:${r.mime};base64,${(r.data || '').trim()}`)); + } + getUserAllTutorials(): Observable { return this.http.get(`${this.baseUrl}/tutorials`); } - getTutorial(id: string | null | undefined): Observable { - return this.http.get(`${this.baseUrl}/tutorial-get/${id}`); + getTutorial(fileName: string | null | undefined): Observable { + return this.http.get(`${this.baseUrl}/tutorial-get/${fileName}`); } create(data: any): Observable { return this.http.post(`${this.baseUrl}/tutorial-add`, data); } - update(id: any,data: any): Observable { - return this.http.put(`${this.baseUrl}/tutorial-update/${id}`, data); + update(fileName: any,data: any): Observable { + return this.http.put(`${this.baseUrl}/tutorial-update/${fileName}`, data); } deleteTutorials(tutorials: Tutorial[]): Observable { return forkJoin( tutorials.map((tutorial) => - this.http.delete(`${this.baseUrl}/tutorials/${tutorial.id}`) + this.http.delete(`${this.baseUrl}/tutorials/${tutorial.fileName}`) ) ); } - deleteTutorial(id: number): Observable { - return this.http.delete(`${this.baseUrl}/tutorials/${id}`); + deleteTutorial(fileName: number): Observable { + return this.http.delete(`${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 +} + diff --git a/jambotron-ui/src/assets/images/1.png b/jambotron-ui/src/assets/images/1.png new file mode 100644 index 0000000..cb09c68 Binary files /dev/null and b/jambotron-ui/src/assets/images/1.png differ diff --git a/jambotron-ui/src/assets/images/favicon.svg b/jambotron-ui/src/assets/images/favicon.svg index 3caf057..135cf81 100644 --- a/jambotron-ui/src/assets/images/favicon.svg +++ b/jambotron-ui/src/assets/images/favicon.svg @@ -4,11 +4,11 @@ - + - + diff --git a/jambotron-ui/src/assets/images/logo-dark.svg b/jambotron-ui/src/assets/images/logo-dark.svg index f766a59..d8cb19e 100644 --- a/jambotron-ui/src/assets/images/logo-dark.svg +++ b/jambotron-ui/src/assets/images/logo-dark.svg @@ -5,11 +5,11 @@ - + - + diff --git a/jambotron-ui/src/assets/images/logo-icon.svg b/jambotron-ui/src/assets/images/logo-icon.svg index 133293d..7e2e62d 100644 --- a/jambotron-ui/src/assets/images/logo-icon.svg +++ b/jambotron-ui/src/assets/images/logo-icon.svg @@ -4,11 +4,11 @@ - + - + diff --git a/jambotron-ui/src/assets/images/logo-white.svg b/jambotron-ui/src/assets/images/logo-white.svg index 5938737..d9ad779 100644 --- a/jambotron-ui/src/assets/images/logo-white.svg +++ b/jambotron-ui/src/assets/images/logo-white.svg @@ -5,11 +5,11 @@ - + - + diff --git a/jambotron-ui/src/index.html b/jambotron-ui/src/index.html index 55b4b5e..c9804e4 100644 --- a/jambotron-ui/src/index.html +++ b/jambotron-ui/src/index.html @@ -10,7 +10,7 @@ - + - diff --git a/jambotron-ui/src/scss/fonts/tabler/tabler-icons.svg b/jambotron-ui/src/scss/fonts/tabler/tabler-icons.svg index 5c004e4..07eee78 100644 --- a/jambotron-ui/src/scss/fonts/tabler/tabler-icons.svg +++ b/jambotron-ui/src/scss/fonts/tabler/tabler-icons.svg @@ -2,7 +2,7 @@ - + @@ -466,7 +466,7 @@ - - > getListFiles() { List 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 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 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)); + } + } } \ No newline at end of file diff --git a/src/main/java/com/jambotronGroup/jambotron/controllers/TutorialController.java b/src/main/java/com/jambotronGroup/jambotron/controllers/TutorialController.java index 4c6d1db..bad1299 100644 --- a/src/main/java/com/jambotronGroup/jambotron/controllers/TutorialController.java +++ b/src/main/java/com/jambotronGroup/jambotron/controllers/TutorialController.java @@ -177,6 +177,7 @@ public class TutorialController { servTutorial.setDescription(tutorial.getDescription()); servTutorial.setPublished(tutorial.isPublished()); servTutorial.setTobepublished(tutorial.isTobepublished()); + servTutorial.setBody(tutorial.getBody()); try { diff --git a/src/main/java/com/jambotronGroup/jambotron/fileUpload/Base64ImageResponse.java b/src/main/java/com/jambotronGroup/jambotron/fileUpload/Base64ImageResponse.java new file mode 100644 index 0000000..04f9a2a --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/fileUpload/Base64ImageResponse.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageService.java b/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageService.java index 33b7354..764d26f 100644 --- a/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageService.java +++ b/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageService.java @@ -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 loadAll(); diff --git a/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageServiceImpl.java b/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageServiceImpl.java index 900c2b9..2697396 100644 --- a/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageServiceImpl.java +++ b/src/main/java/com/jambotronGroup/jambotron/fileUpload/FilesStorageServiceImpl.java @@ -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 ALLOWED_MIME = Set.of( + "image/png", "image/jpeg", "image/webp", "image/gif" + ); + + private static final Map 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; + } + } + } \ No newline at end of file diff --git a/src/main/java/com/jambotronGroup/jambotron/payload/request/Base64ImageRequest.java b/src/main/java/com/jambotronGroup/jambotron/payload/request/Base64ImageRequest.java new file mode 100644 index 0000000..63ac39b --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/payload/request/Base64ImageRequest.java @@ -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; + } +} diff --git a/src/main/java/com/jambotronGroup/jambotron/payload/request/UploadImageResponse.java b/src/main/java/com/jambotronGroup/jambotron/payload/request/UploadImageResponse.java new file mode 100644 index 0000000..69d9453 --- /dev/null +++ b/src/main/java/com/jambotronGroup/jambotron/payload/request/UploadImageResponse.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index f011886..479a5d5 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -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