diff --git a/.drone.yml b/.drone.yml index 55ccbca..d28f2b0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,6 +9,13 @@ steps: - npm install - npm run build + - name: test-chrome + image: timbru31/node-chrome:20-slim + commands: + - npm run test -- --browsers=ChromeHeadlessCI --watch=false + depends_on: + - build + - name: sonar image: sonarsource/sonar-scanner-cli:5 commands: diff --git a/.gitignore b/.gitignore index a368413..25b6c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ yarn-error.log *.launch .settings/ *.sublime-workspace +.nx # Visual Studio Code .vscode/* @@ -40,4 +41,4 @@ testem.log # System files .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db diff --git a/angular.json b/angular.json index fd654ab..2eeb758 100644 --- a/angular.json +++ b/angular.json @@ -16,19 +16,33 @@ "outputPath": "dist/sandkasten", "index": "src/index.html", "browser": "src/main.ts", - "polyfills": ["zone.js"], + "polyfills": [ + "zone.js" + ], "tsConfig": "tsconfig.app.json", - "assets": ["src/favicon.ico", "src/assets"], - "styles": ["src/styles.scss"], + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss" + ], "scripts": [] }, "configurations": { "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], "budgets": [ { "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" + "maximumWarning": "2mb", + "maximumError": "3mb" }, { "type": "anyComponentStyle", @@ -39,6 +53,7 @@ "outputHashing": "all" }, "development": { + "fileReplacements": [], "optimization": false, "extractLicenses": false, "sourceMap": true @@ -67,17 +82,30 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "polyfills": ["zone.js", "zone.js/testing"], + "polyfills": [ + "zone.js", + "zone.js/testing" + ], "tsConfig": "tsconfig.spec.json", - "assets": ["src/favicon.ico", "src/assets"], - "styles": ["src/styles.scss"], - "scripts": [] + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "@angular/material/prebuilt-themes/indigo-pink.css", + "src/styles.scss" + ], + "scripts": [], + "karmaConfig": "karma.conf.js" } }, "lint": { "builder": "@angular-eslint/builder:lint", "options": { - "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] + "lintFilePatterns": [ + "src/**/*.ts", + "src/**/*.html" + ] } } } @@ -85,6 +113,8 @@ }, "cli": { "analytics": false, - "schematicCollections": ["@angular-eslint/schematics"] + "schematicCollections": [ + "@angular-eslint/schematics" + ] } } diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..1896ccd --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,46 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-firefox-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, './coverage/sandkasten'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Chrome', 'Firefox'], + restartOnFileChange: true, + customLaunchers: { + ChromeHeadlessCI: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + } + }); +}; diff --git a/package-lock.json b/package-lock.json index a1bf785..77f415a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,15 @@ "@angular/compiler": "^17.3.7", "@angular/core": "^17.3.7", "@angular/forms": "^17.3.7", + "@angular/material": "^17.3.8", "@angular/platform-browser": "^17.3.7", "@angular/platform-browser-dynamic": "^17.3.7", "@angular/router": "^17.3.7", + "@codemirror/collab": "^6.1.1", "@codemirror/lang-cpp": "^6.0.2", "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/language": "^6.10.2", + "@codemirror/legacy-modes": "^6.4.0", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.26.3", "@emailjs/browser": "^4.3.3", @@ -50,6 +54,7 @@ "karma": "~6.4.2", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.1", + "karma-firefox-launcher": "^2.1.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "prettier": "^3.2.5", @@ -579,6 +584,70 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/material": { + "version": "17.3.8", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-17.3.8.tgz", + "integrity": "sha512-P15p3ixO119DvqtFPCUc+9uKlFgwrwoZtKstcdx/knFlw9c+wS5s9SZzTbB2yqjZoBZ4gC92kqbUQI2o7AUbUQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/auto-init": "15.0.0-canary.7f224ddd4.0", + "@material/banner": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/card": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/chips": "15.0.0-canary.7f224ddd4.0", + "@material/circular-progress": "15.0.0-canary.7f224ddd4.0", + "@material/data-table": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dialog": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/drawer": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/fab": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/form-field": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/image-list": "15.0.0-canary.7f224ddd4.0", + "@material/layout-grid": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/radio": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/segmented-button": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/slider": "15.0.0-canary.7f224ddd4.0", + "@material/snackbar": "15.0.0-canary.7f224ddd4.0", + "@material/switch": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-bar": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/textfield": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tooltip": "15.0.0-canary.7f224ddd4.0", + "@material/top-app-bar": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^17.0.0 || ^18.0.0", + "@angular/cdk": "17.3.8", + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "@angular/forms": "^17.0.0 || ^18.0.0", + "@angular/platform-browser": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/platform-browser": { "version": "17.3.8", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.8.tgz", @@ -2448,6 +2517,15 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@codemirror/collab": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/collab/-/collab-6.1.1.tgz", + "integrity": "sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0" + } + }, "node_modules/@codemirror/commands": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.5.0.tgz", @@ -2483,9 +2561,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz", - "integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", + "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -2495,6 +2573,14 @@ "style-mod": "^4.0.0" } }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.4.0.tgz", + "integrity": "sha512-5m/K+1A6gYR0e+h/dEde7LoGimMjRtWXZFg4Lo70cc8HzjSdHe3fLwjWMR0VRl5KFT1SxalSap7uMgPKF28wBA==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, "node_modules/@codemirror/lint": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.7.0.tgz", @@ -3374,6 +3460,758 @@ "node": ">= 0.4" } }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-1GSJaPKef+7HRuV+HusVZHps64cmZuOItDbt40tjJVaikcaZvwmHlcTxRIqzcRoCdt5ZKHh3NoO7GB9Khg4Jnw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-t7ZGpRJ3ec0QDUO0nJu/SMgLW7qcuG2KqIsEYD1Ej8qhI2xpdR2ydSDQOkVEitXmKoGol1oq4nYSBjTlB65GqA==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-g9wBUZzYBizyBcBQXTIafnRUUPi7efU9gPJfzeGgkynXiccP/vh5XMmH+PBxl5v+4MlP/d4cZ2NUYoAN7UTqSA==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-I9KQOKXpLfJkP8MqZyr8wZIzdPHrwPjFvGd9zSK91/vPyE4hzHRJc/0njsh9g8Lm9PRYLbifXX+719uTbHxx+A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BHB7iyHgRVH+JF16+iscR+Qaic+p7LU1FOLgP8KucRlpF9tTwIxQA6mJwGRi5gUtcG+vyCmzVS+hIQ6DqT/7BA==", + "dependencies": { + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kt7y9/IWOtJTr3Z/AoWJT3ZLN7CLlzXhx2udCLP9ootZU2bfGK0lzNwmo80bv/pJfrY9ihQKCtuGTtNxUy+vIw==", + "dependencies": { + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-rURcrL5O1u6hzWR+dNgiQ/n89vk6tdmdP3mZgnxJx61q4I/k1yijKqNJSLrkXH7Rto3bM5NRKMOlgvMvVd7UMQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AYAivV3GSk/T/nRIpH27sOHFPaSMrE3L0WYbnb5Wa93FgY8a0fbsFYtSH2QmtwnzXveg+B1zGTt7/xIIcynKdQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DJrqCKb+LuGtjNvKl8XigvyK02y36GRkfhMUYTcJEi3PrOE00bwXtyj7ilhzEVshQiXg6AHGWXtf5UqwNrx3Ow==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-/2WZsuBIq9z9RWYF5Jo6b7P6u0fwit+29/mN7rmAZ6akqUR54nXyNfoSNiyydMkzPlZZsep5KrSHododDhBZbA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/checkbox": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/linear-progress": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/select": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-o9EXmGKVpiQ6mHhyV3oDDzc78Ow3E7v8dlaOhgaDSXgmqaE8v5sIlLNa/LKSyUga83/fpGk3QViSGXotpQx0jA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-u0XpTlv1JqWC/bQ3DavJ1JguofTelLT2wloj59l3/1b60jv42JQ6Am7jU3I8/SIUB1MKaW7dYocXjDWtWJakLA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-mQ1HT186GPQSkRg5S18i70typ5ZytfjL09R0gJ2Qg5/G+MLCGi7TAjZZSH65tuD/QGOjel4rDdWOTmYbPYV6HA==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-qyO0W0KBftfH8dlLR0gVAgv7ZHNvU8ae11Ao6zJif/YxcvK4+gph1z8AO4H410YmC2kZiwpSKyxM1iQCCzbb4g==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-tV6s4/pUBECedaI36Yj18KmRCk1vfue/JP/5yYRlFNnLMRVISePbZaKkn/BHXVf+26I3W879+XqIGlDVdmOoMA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-4h76QrzfZTcPdd+awDPZ4Q0YdSqsXQnS540TPtyXUJ/5G99V6VwGpjMPIxAsW0y+pmI9UkLL/srrMaJec+7r4Q==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SAjtxYh6YlKZriU83diDEQ7jNSP2MnxKsER0TvFeyG1vX/DWsUyYDOIJTOEa9K1N+fgJEBkNK8hY55QhQaspew==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-0KMo5ijjYaEHPiZ2pCVIcbaTS2LycvH9zEhEMKwPPGssBCX7iz5ffYQFk7e5yrQand1r3jnQQgYfHAwtykArnQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Jmg1nltq4J6S6A10EGMZnvufrvU3YTi+8R8ZD9lkSbun0Fm2TVdICQt/Auyi6An9zP66oQN6c31eqO6KfIPsDg==", + "dependencies": { + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-fEPWgDQEPJ6WF7hNnIStxucHR9LE4DoDSMqCsGWS2Yu+NLZYLuCEecgR0UqQsl1EQdNRaFh8VH93KuxGd2hiPg==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-DcK7IL4ICY/DW+48YQZZs9g0U1kRaW0Wb0BxhvppDMYziHo/CTpFdle4gjyuTyRxPOdHQz5a97ru48Z9O4muTw==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-voMjG2p80XbjL1B2lmF65zO5gEgJOVKClLdqh4wbYzYfwY/SR9c8eLvlYG7DLdFaFBl/7gGxD8TvvZ329HUFPw==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-veDABLxMn2RmvfnUO2RUmC1OFfWr4cU+MrxKPoDD2hl3l3eDYv5fxws6r5T1JoSyXoaN+oEZpheS0+M9Ure8Pg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-f60hVJhIU6I3/17Tqqzch1emUKEcfVVgHVqADbU14JD+oEIz429ZX9ksZ3VChoU3+eejFl+jVdZMLE/LrAuwpg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-pRDEwPQielDiC9Sc5XhCXrGxP8wWOnAO8sQlMebfBYHYqy5hhiIzibezS8CSaW4MFQFyXmCmpmqWlbqGYRmiyg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/progress-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Is0NV91sJlXF5pOebYAtWLF4wU2MJDbYqztML/zQNENkQxDOvEXu3nWNb3YScMIYJJXvARO0Liur5K4yPagS1Q==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-D11QU1dXqLbh5X1zKlEhS3QWh0b5BPNXlafc5MXfkdJHhOiieb7LC9hMJhbrHtj24FadJ7evaFW/T2ugJbJNnQ==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-7RZHvw0gbwppaAJ/Oh5SWmfAKJ62aw1IMB3+3MRwsb5PLoV666wInYa+zJfE4i7qBeOn904xqT2Nko5hY0ssrg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-Yg2usuKB2DKlKIBISbie9BFsOVuffF71xjbxPbybvqemxqUBd+bD5/t6H1fLE+F8/NCu5JMigho4ewUU+0RCiw==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UPbDjE5CqT+SqTs0mNFG6uFEw7wBlgYmh+noSkQ6ty/EURm8lF125dmi4dv4kW0+octonMXqkGtAoZwLIHKf/w==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-wR1X0Sr0KmQLu6+YOFKAI84G3L6psqd7Kys5kfb8WKBM36zxO5HQXC5nJm/Y0rdn22ixzsIz2GBo0MNU4V4k1A==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-JqOsWM1f4aGdotP0rh1vZlPZTg6lZgh39FIYHFMfOwfhR+LAikUJ+37ciqZuewgzXB6iiRO6a8aUH6HR5SJYPg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-UVf14qAtmPiaaZjuJtmN36HETyoKWmsZM/qn1L5ciR2URb8O035dFWnz4ZWFMmAYBno/L7JiZaCkPurv2ZNrGA==", + "dependencies": { + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LCnVRUSAhELTKI/9hSvyvIvQIpPpqF29BV+O9yM4WoNNmNWqTulvuiv7grHZl6Z+kJuxSg4BGbsPxxb9dXozPg==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/touch-target": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-WioZtQEXRpglum0cMSzSqocnhsGRr+ZIhvKb3FlaNrTaK8H3Y4QA7rVjv3emRtrLOOjaT6/RiIaUMTo9AGzWQQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/list": "15.0.0-canary.7f224ddd4.0", + "@material/menu": "15.0.0-canary.7f224ddd4.0", + "@material/menu-surface": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8z8l1W3+cymObunJoRhwFPKZ+FyECfJ4MJykNiaZq7XJFZkV6xNmqAVrrbQj93FtLsECn9g4PjjIomguVn/OEw==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-QU/WSaSWlLKQRqOhJrPgm29wqvvzRusMqwAcrCh1JTrCl+xwJ43q5WLDfjYhubeKtrEEgGu9tekkAiYfMG7EBw==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-sm7EbVKddaXpT/aXAYBdPoN0k8yeg9+dprgBUkrdqGzWJAeCkxb4fv2B3He88YiCtvkTz2KLY4CThPQBSEsMFQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/icon-button": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-lEDJfRvkVyyeHWIBfoxYjJVl+WlEAE2kZ/+6OqB1FW0OV8ftTODZGhHRSzjVBA1/p4FPuhAtKtoK9jTpa4AZjA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-E1xGACImyCLurhnizyOTCgOiVezce4HlBFAI6YhJo/AyVwjN2Dtas4ZLQMvvWWqpyhITNkeYdOchwCC1mrz3AQ==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/focus-ring": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-p1Asb2NzrcECvAQU3b2SYrpyJGyJLQWR+nXTYzDKE8WOpLIRCXap2audNqD7fvN/A20UJ1J8U01ptrvCkwJ4eA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "@material/tab-indicator": "15.0.0-canary.7f224ddd4.0", + "@material/tab-scroller": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-h9Td3MPqbs33spcPS7ecByRHraYgU4tNCZpZzZXw31RypjKvISDv/PS5wcA4RmWqNGih78T7xg4QIGsZg4Pk4w==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-LFeYNjQpdXecwECd8UaqHYbhscDCwhGln5Yh+3ctvcEgvmDPNjhKn/DL3sWprWvG8NAhP6sHMrsGhQFVdCWtTg==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/tab": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-AExmFvgE5nNF0UA4l2cSzPghtxSUQeeoyRjFLHLy+oAaE4eKZFrSy0zEpqPeWPQpEMDZk+6Y+6T3cOFYBeSvsw==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/density": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/floating-label": "15.0.0-canary.7f224ddd4.0", + "@material/line-ripple": "15.0.0-canary.7f224ddd4.0", + "@material/notched-outline": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-hs45hJoE9yVnoVOcsN1jklyOa51U4lzWsEnQEuJTPOk2+0HqCQ0yv/q0InpSnm2i69fNSyZC60+8HADZGF8ugQ==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-r9TDoicmcT7FhUXC4eYMFnt9TZsz0G8T3wXvkKncLppYvZ517gPyD/1+yhuGfGOxAzxTrM66S/oEc1fFE2q4hw==", + "dependencies": { + "@material/elevation": "15.0.0-canary.7f224ddd4.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-8qNk3pmPLTnam3XYC1sZuplQXW9xLn4Z4MI3D+U17Q7pfNZfoOugGr+d2cLA9yWAEjVJYB0mj8Yu86+udo4N9w==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/button": "15.0.0-canary.7f224ddd4.0", + "@material/dom": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/tokens": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-SARR5/ClYT4CLe9qAXakbr0i0cMY0V3V4pe3ElIJPfL2Z2c4wGR1mTR8m2LxU1MfGKK8aRoUdtfKaxWejp+eNA==", + "dependencies": { + "@material/animation": "15.0.0-canary.7f224ddd4.0", + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/elevation": "15.0.0-canary.7f224ddd4.0", + "@material/ripple": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/shape": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "@material/typography": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-BJo/wFKHPYLGsRaIpd7vsQwKr02LtO2e89Psv0on/p0OephlNIgeB9dD9W+bQmaeZsZ6liKSKRl6wJWDiK71PA==", + "dependencies": { + "@material/base": "15.0.0-canary.7f224ddd4.0", + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/rtl": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.7f224ddd4.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.7f224ddd4.0.tgz", + "integrity": "sha512-kBaZeCGD50iq1DeRRH5OM5Jl7Gdk+/NOfKArkY4ksBZvJiStJ7ACAhpvb8MEGm4s3jvDInQFLsDq3hL+SA79sQ==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.7f224ddd4.0", + "@material/theme": "15.0.0-canary.7f224ddd4.0", + "tslib": "^2.1.0" + } + }, "node_modules/@ngtools/webpack": { "version": "17.3.7", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.7.tgz", @@ -5699,12 +6537,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -7905,9 +8743,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -9651,6 +10489,33 @@ "node": "*" } }, + "node_modules/karma-firefox-launcher": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz", + "integrity": "sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^2.2.0", + "which": "^3.0.0" + } + }, + "node_modules/karma-firefox-launcher/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/karma-jasmine": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", @@ -12580,6 +13445,11 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" + }, "node_modules/sass": { "version": "1.71.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", diff --git a/package.json b/package.json index dcd6964..4105204 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,15 @@ "@angular/compiler": "^17.3.7", "@angular/core": "^17.3.7", "@angular/forms": "^17.3.7", + "@angular/material": "^17.3.8", "@angular/platform-browser": "^17.3.7", "@angular/platform-browser-dynamic": "^17.3.7", "@angular/router": "^17.3.7", + "@codemirror/collab": "^6.1.1", "@codemirror/lang-cpp": "^6.0.2", "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/language": "^6.10.2", + "@codemirror/legacy-modes": "^6.4.0", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.26.3", "@emailjs/browser": "^4.3.3", @@ -54,9 +58,10 @@ "karma": "~6.4.2", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.1", + "karma-firefox-launcher": "^2.1.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "prettier": "^3.2.5", "typescript": "~5.4.5" } -} +} \ No newline at end of file diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 7467ecf..f724462 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,11 +1,18 @@ import { TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; import { AppComponent } from './app.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterModule } from '@angular/router'; +import { HttpClientModule } from '@angular/common/http'; describe('AppComponent', () => { beforeEach(() => TestBed.configureTestingModule({ - imports: [RouterTestingModule, AppComponent], + imports: [ + RouterModule.forRoot([]), + AppComponent, + HttpClientModule, + TranslateModule.forRoot(), + ], }) ); @@ -19,8 +26,6 @@ describe('AppComponent', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('.content span')?.textContent).toContain( - 'sandkasten app is running!' - ); + expect(compiled.textContent).toContain('HeaderPage.Editor'); }); }); diff --git a/src/app/app-routing.module.ts b/src/app/app.routes.ts similarity index 70% rename from src/app/app-routing.module.ts rename to src/app/app.routes.ts index 73a48dc..a205dc1 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app.routes.ts @@ -1,6 +1,4 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { WorkComponent } from './components/work/work.component'; +import { Routes } from '@angular/router'; import { EditorComponent } from './components/editor/editor.component'; import { LandingPageComponent } from './components/landing-page/landing-page.component'; import { DocumentationComponent } from './components/documentation/documentation.component'; @@ -8,21 +6,20 @@ import { FormComponent } from './components/form/form.component'; import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component'; import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component'; import { WorksListComponent } from './components/works-list/works-list.component'; +import { RegisterComponent } from './components/register/register.component'; +import { LoginComponent } from './components/login/login.component'; // Toutes les routes de l'application sont définies ici -const routes: Routes = [ +export const routes: Routes = [ { path: '', component: LandingPageComponent }, - { path: 'work/:link', component: WorkComponent }, + { path: 'work/:work', component: EditorComponent }, { path: 'works', component: WorksListComponent }, { path: 'editor', component: EditorComponent }, + { path: 'editor-live/:idRoom', component: EditorComponent }, { path: 'documentation', component: DocumentationComponent }, { path: 'contact', component: FormComponent }, { path: 'terms-of-service', component: TermsOfServiceComponent }, { path: 'privacy-policy', component: PrivacyPolicyComponent }, + { path: 'register', component: RegisterComponent }, + { path: 'login', component: LoginComponent }, ]; - -@NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule], -}) -export class AppRoutingModule {} diff --git a/src/app/components/editor/editor.component.html b/src/app/components/editor/editor.component.html index dc07871..f7cfc39 100644 --- a/src/app/components/editor/editor.component.html +++ b/src/app/components/editor/editor.component.html @@ -80,6 +80,10 @@ } + + + + Login is required + + + + + Password + + + + Password is required + + + + + +
{{ errorLogin }}
+
{{ successLogin }}
+ diff --git a/src/app/components/login/login.component.spec.ts b/src/app/components/login/login.component.spec.ts new file mode 100644 index 0000000..1166f9c --- /dev/null +++ b/src/app/components/login/login.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; +import { HttpClientModule } from '@angular/common/http'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent, HttpClientModule, NoopAnimationsModule], + }).compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts new file mode 100644 index 0000000..51179e1 --- /dev/null +++ b/src/app/components/login/login.component.ts @@ -0,0 +1,71 @@ +import { Component } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + FormControl, + Validators, + FormsModule, + ReactiveFormsModule, + NgForm, + FormGroup, + FormBuilder, +} from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { merge } from 'rxjs'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; +import { UserService } from 'src/app/services/user.service'; +import { User } from 'src/app/models/user.model'; + +@Component({ + selector: 'app-auth', + templateUrl: './login.component.html', + styleUrl: './login.component.css', + standalone: true, + imports: [ + MatFormFieldModule, + MatInputModule, + FormsModule, + ReactiveFormsModule, + MatButtonModule, + MatIconModule, + CommonModule, + ], +}) +export class LoginComponent { + hide = true; + + loginForm = this.formBuilder.group({ + login: ['', Validators.required], + password: ['', Validators.required], + }); + + errorMessage = ''; + + successLogin = ''; + errorLogin = ''; + + constructor( + private userService: UserService, + private formBuilder: FormBuilder + ) { + } + + loginAction() { + const formValue = this.loginForm.value; + + this.userService + .loginUser(formValue.login!, formValue.password!) + .subscribe((response) => { + console.log('response :', response); + if (response.success) { + this.successLogin = 'Vous êtes connecté.'; + this.errorLogin = ''; + } else { + this.errorLogin = "L'authentification a échoué."; + this.successLogin = ''; + } + }); + } +} diff --git a/src/app/components/privacy-policy/privacy-policy.component.spec.ts b/src/app/components/privacy-policy/privacy-policy.component.spec.ts index 54a75e8..c93d9f4 100644 --- a/src/app/components/privacy-policy/privacy-policy.component.spec.ts +++ b/src/app/components/privacy-policy/privacy-policy.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PrivacyPolicyComponent } from './privacy-policy.component'; +import { TranslateModule } from '@ngx-translate/core'; describe('PrivacyPolicyComponent', () => { let component: PrivacyPolicyComponent; @@ -8,7 +9,7 @@ describe('PrivacyPolicyComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PrivacyPolicyComponent], + imports: [PrivacyPolicyComponent, TranslateModule.forRoot()], }).compileComponents(); fixture = TestBed.createComponent(PrivacyPolicyComponent); diff --git a/src/app/components/register/register.component.css b/src/app/components/register/register.component.css new file mode 100644 index 0000000..5eb5cdd --- /dev/null +++ b/src/app/components/register/register.component.css @@ -0,0 +1,18 @@ +.form-container { + display: flex; + flex-direction: column; +} + +.form-field { + width: 100%; +} + +.success-message { + color: green; + margin-top: 20px; +} + +.error-message { + color: red; + margin-top: 20px; +} diff --git a/src/app/components/register/register.component.html b/src/app/components/register/register.component.html new file mode 100644 index 0000000..1c3bf41 --- /dev/null +++ b/src/app/components/register/register.component.html @@ -0,0 +1,53 @@ +
+

Formulaire d'inscription :

+ + + Enter your email + + {{ + errorMessage + }} + + + + Enter your login + + {{ + errorMessage + }} + + + + Enter your password + + + {{ + errorMessage + }} + + + + + +
+ {{ successRegister }} +
+
+ {{ errorRegister }} +
+
diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts new file mode 100644 index 0000000..d5467a4 --- /dev/null +++ b/src/app/components/register/register.component.ts @@ -0,0 +1,75 @@ +import { Component } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + FormControl, + Validators, + FormsModule, + ReactiveFormsModule, + NgForm, + FormBuilder, +} from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { merge } from 'rxjs'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; +import { UserService } from 'src/app/services/user.service'; +import { User } from 'src/app/models/user.model'; + +@Component({ + selector: 'app-auth', + templateUrl: './register.component.html', + styleUrl: './register.component.css', + standalone: true, + imports: [ + MatFormFieldModule, + MatInputModule, + FormsModule, + ReactiveFormsModule, + MatButtonModule, + MatIconModule, + CommonModule, + ], +}) +export class RegisterComponent { + hide = true; + + registerForm = this.formBuilder.group({ + email: ['', Validators.required], + login: ['', Validators.required], + password: ['', Validators.required], + }); + errorMessage = ''; + + successRegister = ''; + errorRegister = ''; + + constructor( + private userService: UserService, + private formBuilder: FormBuilder + ) { + } + + register() { + const formRegisterValue = this.registerForm.value; + + this.userService + .postUser( + formRegisterValue.email!, + formRegisterValue.login!, + formRegisterValue.password! + ) + .subscribe((response) => { + console.log('response :', response); + if (response.success) { + this.successRegister = 'Votre compte a été créé avec succès.'; + this.errorRegister = ''; + } else { + this.errorRegister = + "L'inscription a échoué : un compte avec ce login existe déjà."; + this.successRegister = ''; + } + }); + } +} diff --git a/src/app/components/terms-of-service/terms-of-service.component.spec.ts b/src/app/components/terms-of-service/terms-of-service.component.spec.ts index 7faa79d..1fcbf16 100644 --- a/src/app/components/terms-of-service/terms-of-service.component.spec.ts +++ b/src/app/components/terms-of-service/terms-of-service.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TermsOfServiceComponent } from './terms-of-service.component'; +import { TranslateModule } from '@ngx-translate/core'; describe('TermsOfServiceComponent', () => { let component: TermsOfServiceComponent; @@ -8,7 +9,7 @@ describe('TermsOfServiceComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TermsOfServiceComponent], + imports: [TermsOfServiceComponent, TranslateModule.forRoot()], }).compileComponents(); fixture = TestBed.createComponent(TermsOfServiceComponent); diff --git a/src/app/components/work-list-detail/work-list-detail.component.html b/src/app/components/work-list-detail/work-list-detail.component.html index 613041f..9dcd285 100644 --- a/src/app/components/work-list-detail/work-list-detail.component.html +++ b/src/app/components/work-list-detail/work-list-detail.component.html @@ -1,9 +1,9 @@
-

{{ work.title }}

+

{{ work?.title }}

{{ - work.content | slice: 0 : 50 + work?.content | slice: 0 : 50 }} -
diff --git a/src/app/components/work-list-detail/work-list-detail.component.spec.ts b/src/app/components/work-list-detail/work-list-detail.component.spec.ts index d546f31..cf4a10d 100644 --- a/src/app/components/work-list-detail/work-list-detail.component.spec.ts +++ b/src/app/components/work-list-detail/work-list-detail.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { WorkListDetailComponent } from './work-list-detail.component'; +import { HttpClientModule } from '@angular/common/http'; +import { RouterModule } from '@angular/router'; describe('WorkListDetailComponent', () => { let component: WorkListDetailComponent; @@ -8,7 +9,7 @@ describe('WorkListDetailComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [WorkListDetailComponent], + imports: [WorkListDetailComponent, HttpClientModule, RouterModule.forRoot([])], }).compileComponents(); fixture = TestBed.createComponent(WorkListDetailComponent); diff --git a/src/app/components/work-list-detail/work-list-detail.component.ts b/src/app/components/work-list-detail/work-list-detail.component.ts index 77cd65c..3ac9d7e 100644 --- a/src/app/components/work-list-detail/work-list-detail.component.ts +++ b/src/app/components/work-list-detail/work-list-detail.component.ts @@ -11,5 +11,5 @@ import { SlicePipe } from '@angular/common'; styleUrl: './work-list-detail.component.scss', }) export class WorkListDetailComponent { - @Input() work!: Work; + @Input() work?: Work; } diff --git a/src/app/components/work/work.component.html b/src/app/components/work/work.component.html deleted file mode 100644 index 7f9c5cf..0000000 --- a/src/app/components/work/work.component.html +++ /dev/null @@ -1,4 +0,0 @@ -
-

{{ work.title }}

- -
diff --git a/src/app/components/work/work.component.scss b/src/app/components/work/work.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/components/work/work.component.spec.ts b/src/app/components/work/work.component.spec.ts deleted file mode 100644 index 0498a4c..0000000 --- a/src/app/components/work/work.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { WorkComponent } from './work.component'; - -describe('WorkComponent', () => { - let component: WorkComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [WorkComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(WorkComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/work/work.component.ts b/src/app/components/work/work.component.ts deleted file mode 100644 index 0e13aa2..0000000 --- a/src/app/components/work/work.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { RouterLink, ActivatedRoute } from '@angular/router'; -import { ThemeService } from '../../services/theme.service'; -import { NgClass, NgIf } from '@angular/common'; -import { TranslateModule } from '@ngx-translate/core'; -import { Work } from '../../models/work.model'; -import { WorkService } from '../../services/work.service'; -import { NgForOf } from '@angular/common'; -import { FormsModule, NgForm } from '@angular/forms'; -import { EditorComponent } from '../editor/editor.component'; -import { WorkListDetailComponent } from '../work-list-detail/work-list-detail.component'; - -@Component({ - selector: 'app-work', - templateUrl: './work.component.html', - styleUrl: './work.component.scss', - standalone: true, - imports: [ - NgClass, - TranslateModule, - RouterLink, - NgForOf, - FormsModule, - EditorComponent, - WorkListDetailComponent, - NgIf, - ], -}) -export class WorkComponent implements OnInit { - // à retirer quand les boutons seront dans editor.component - isLoaded: boolean = false; // Pour vérifier si le chargement est terminé - - themeClass!: string; - work!: Work; - - constructor( - private route: ActivatedRoute, - private themeService: ThemeService, - protected workService: WorkService - ) {} - - ngOnInit() { - this.themeService.isDarkTheme.subscribe((value) => { - value - ? (this.themeClass = 'dark-theme') - : (this.themeClass = 'light-theme'); - }); - - const work_link = String(this.route.snapshot.paramMap.get('link')); - - this.workService.getWorkByLink(work_link).subscribe((response: Work) => { - this.work = response as Work; - }); - } - - onSubmit(form: NgForm) { - this.workService.saveWork(form); - } -} diff --git a/src/app/components/works-list/works-list.component.spec.ts b/src/app/components/works-list/works-list.component.spec.ts index 9bbe47c..cc3130c 100644 --- a/src/app/components/works-list/works-list.component.spec.ts +++ b/src/app/components/works-list/works-list.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { WorksListComponent } from './works-list.component'; +import { RouterModule } from '@angular/router'; +import { HttpClientModule } from '@angular/common/http'; describe('WorksListComponent', () => { let component: WorksListComponent; @@ -8,7 +10,7 @@ describe('WorksListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [WorksListComponent], + imports: [WorksListComponent, HttpClientModule, RouterModule.forRoot([])], }).compileComponents(); fixture = TestBed.createComponent(WorksListComponent); diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts new file mode 100644 index 0000000..55c3826 --- /dev/null +++ b/src/app/models/user.model.ts @@ -0,0 +1,6 @@ +export interface User { + id_user: number; + login: string; + password: string; + permissions: number; +} diff --git a/src/app/services/codeExecution.service.ts b/src/app/services/backendService.service.ts similarity index 70% rename from src/app/services/codeExecution.service.ts rename to src/app/services/backendService.service.ts index 3d57318..d6808f4 100644 --- a/src/app/services/codeExecution.service.ts +++ b/src/app/services/backendService.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { SSE } from 'sse.js'; import { Observable, Subject } from 'rxjs'; +import { environment } from '../../environments/environment'; export type ExecutionMessage = { type: 'stdout' | 'stderr' | 'exit'; @@ -10,14 +11,26 @@ export type ExecutionMessage = { @Injectable({ providedIn: 'root', }) -export class CodeExecutionService { - private apiUrl = 'http://localhost:3000/run'; +export class BackendService { + private apiUrl = environment.apiUrl; + private resultSubject = new Subject(); constructor() {} + async createRoom(code: string) { + const reponse = await fetch(`${this.apiUrl}/live`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ code }), + }); + return reponse.text(); + } + executeCode(code: string, language: string) { - const sse = new SSE(this.apiUrl, { + const sse = new SSE(`${this.apiUrl}/run`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/app/services/connection.service.ts b/src/app/services/connection.service.ts new file mode 100644 index 0000000..a3b15c8 --- /dev/null +++ b/src/app/services/connection.service.ts @@ -0,0 +1,120 @@ +import { ChangeSet, Text } from '@codemirror/state'; +import { EditorView } from 'codemirror'; +import { + Update, + collab, + getSyncedVersion, + receiveUpdates, + sendableUpdates, +} from '@codemirror/collab'; +import { ViewPlugin, ViewUpdate } from '@codemirror/view'; + +export class Connection { + private requestId = 0; + private resolves: Record void> = {}; + + constructor(private client: WebSocket) { + client.addEventListener('message', (event) => { + const response = JSON.parse(event.data); + if ('_request' in response) { + const resolve = this.resolves[response._request]; + if (resolve) { + resolve(response.payload); + } else { + console.error( + 'Received response for unknown or already used request', + response._request + ); + } + } else { + console.error('Received invalid response', response._request); + } + }); + } + + request(body: Record): Promise { + body['_request'] = this.requestId; + this.client.send(JSON.stringify(body)); + return new Promise( + (resolve) => (this.resolves[this.requestId++] = resolve) + ); + } +} + +function pushUpdates( + connection: Connection, + version: number, + fullUpdates: readonly Update[] +): Promise { + // Strip off transaction data + let updates = fullUpdates.map((u) => ({ + clientID: u.clientID, + changes: u.changes.toJSON(), + })); + return connection.request({ type: 'pushUpdates', version, updates }); +} + +function pullUpdates( + connection: Connection, + version: number +): Promise { + return connection.request({ type: 'pullUpdates', version }).then((updates) => + updates.map((u: any) => ({ + changes: ChangeSet.fromJSON(u.changes), + clientID: u.clientID, + })) + ); +} + +export function getDocument( + connection: Connection +): Promise<{ version: number; doc: Text }> { + return connection.request({ type: 'getDocument' }).then((data) => ({ + version: data.version, + doc: Text.of(data.doc.split('\n')), + })); +} + +export function peerExtension(startVersion: number, connection: Connection) { + let plugin = ViewPlugin.fromClass( + class { + private pushing = false; + private done = false; + + constructor(private view: EditorView) { + this.pull(); + } + + update(update: ViewUpdate) { + if (update.docChanged) this.push(); + } + + async push() { + let updates = sendableUpdates(this.view.state); + + if (this.pushing || !updates.length) return; + this.pushing = true; + let version = getSyncedVersion(this.view.state); + await pushUpdates(connection, version, updates); + this.pushing = false; + // Regardless of whether the push failed or new updates came in + // while it was running, try again if there's updates remaining + if (sendableUpdates(this.view.state).length) + setTimeout(() => this.push(), 100); + } + + async pull() { + while (!this.done) { + let version = getSyncedVersion(this.view.state); + let updates = await pullUpdates(connection, version); + this.view.dispatch(receiveUpdates(this.view.state, updates)); + } + } + + destroy() { + this.done = true; + } + } + ); + return [collab({ startVersion }), plugin]; +} diff --git a/src/app/services/user.service.ts b/src/app/services/user.service.ts new file mode 100644 index 0000000..d2ca330 --- /dev/null +++ b/src/app/services/user.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { User } from '../models/user.model'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { NgForm } from '@angular/forms'; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + API_URL = 'http://127.0.0.1:3000'; + + constructor(private http: HttpClient) {} + + postUser( + email: string, + login: string, + password: string + ): Observable { + const body = { + email: email, + login: login, + password: password, + permissions: 0, + }; + + return this.http.post(`${this.API_URL}/users`, body); + } + + loginUser(login: string, password: string): Observable { + const body = { + login: login, + password: password, + }; + + return this.http.post(`${this.API_URL}/users/login`, body, { + withCredentials: true, + }); + } + + logoutUser(): Observable { + return this.http.post(`${this.API_URL}/users/logout`, { + withCredentials: true, + }); + } +} + +type Response = { + success: boolean; +}; diff --git a/src/app/services/work.service.spec.ts b/src/app/services/work.service.spec.ts index f6cda06..6ddb976 100644 --- a/src/app/services/work.service.spec.ts +++ b/src/app/services/work.service.spec.ts @@ -1,12 +1,15 @@ import { TestBed } from '@angular/core/testing'; import { WorkService } from './work.service'; +import { HttpClientModule } from '@angular/common/http'; describe('WorkService', () => { let service: WorkService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [HttpClientModule] + }); service = TestBed.inject(WorkService); }); diff --git a/src/app/services/work.service.ts b/src/app/services/work.service.ts index 2422ce6..b46ff13 100644 --- a/src/app/services/work.service.ts +++ b/src/app/services/work.service.ts @@ -14,30 +14,30 @@ export class WorkService { constructor(private http: HttpClient) {} - getWorks(): Observable { - return this.http.get(`${this.API_URL}/works`); + getWorks(): Observable { + return this.http.get(`${this.API_URL}/works`); } - getWorkByLink(link: string): Observable { - return this.http.get(`${this.API_URL}/works/${link}`); + getWorkByLink(link: string): Observable { + return this.http.get(`${this.API_URL}/works/${link}`); } saveWork(form: NgForm): void { const code = form.value.content; - this.http.post(`${this.API_URL}/works/save`, code).subscribe(); + this.http.post(`${this.API_URL}/works/save`, code).subscribe(); } postWork(code: string, language: string, id_user: number): string { - let body = { - id_user: id_user, // tant que ça pas résolu -> je peux pas faire le share + const body = { + id_user, // tant que ça pas résolu -> je peux pas faire le share link: crypto.randomUUID(), language: language, title: `Basic ${language}`, code: code, }; - this.http.post(`${this.API_URL}/works`, body).subscribe(); + this.http.post(`${this.API_URL}/works`, body).subscribe(); return body.link; } diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts new file mode 100644 index 0000000..0900c39 --- /dev/null +++ b/src/environments/environment.prod.ts @@ -0,0 +1,5 @@ +export const environment = { + production: true, + apiUrl: 'https://tododomain.com', + webSocketUrl: 'ws://tododomain.com', +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts new file mode 100644 index 0000000..0411da0 --- /dev/null +++ b/src/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + apiUrl: 'http://localhost:3000', + webSocketUrl: 'ws://localhost:3000', +}; diff --git a/src/index.html b/src/index.html index 4adf77b..464867f 100644 --- a/src/index.html +++ b/src/index.html @@ -11,8 +11,14 @@ + + - + diff --git a/src/main.ts b/src/main.ts index 4d15ebd..713ebd7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,15 +8,17 @@ import { HttpClient, } from '@angular/common/http'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; -import { AppRoutingModule } from './app/app-routing.module'; import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'; import { TranslationService } from './app/services/translation.service'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { routes } from './app/app.routes'; bootstrapApplication(AppComponent, { providers: [ + provideRouter(routes, withComponentInputBinding()), importProvidersFrom( BrowserModule, - AppRoutingModule, ReactiveFormsModule, FormsModule, TranslateModule.forRoot({ @@ -30,5 +32,6 @@ bootstrapApplication(AppComponent, { ), TranslationService, provideHttpClient(withInterceptorsFromDi()), + provideAnimationsAsync(), ], }).catch(console.error);