From b5d7b15c99a4b95de09c855892003b8c1e0fef98 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Sun, 21 Nov 2021 09:34:59 +0100 Subject: [PATCH 01/25] feat: level and gear score in the company character table are now always displayed alongside with weapon skills --- .../characters-table/characters-table.component.css | 4 +--- .../characters-table/characters-table.component.ts | 13 ++++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/webapp/src/app/pages/company/components/characters-table/characters-table.component.css b/webapp/src/app/pages/company/components/characters-table/characters-table.component.css index b993817..2f95087 100644 --- a/webapp/src/app/pages/company/components/characters-table/characters-table.component.css +++ b/webapp/src/app/pages/company/components/characters-table/characters-table.component.css @@ -11,7 +11,6 @@ flex: 0 0 120px !important; width: 120px !important; overflow-wrap: break-word; - word-wrap: break-word; word-break: break-word; @@ -39,7 +38,6 @@ flex: 0 0 240px !important; width: 240px !important; overflow-wrap: break-word; - word-wrap: break-word; word-break: break-word; @@ -56,7 +54,7 @@ mat-button-toggle-group { } .mat-button-toggle { - flex: 0 1 33.33%; + flex: 0 1 25%; border-bottom: solid 1px rgba(255, 255, 255, 0.12); border-right: solid 1px rgba(255, 255, 255, 0.12); } diff --git a/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts b/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts index ba2561e..4087633 100644 --- a/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts +++ b/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts @@ -32,15 +32,14 @@ export type FilterModel = { styleUrls: ['./characters-table.component.css'] }) export class CharactersTableComponent implements OnInit, AfterViewInit { - displayedColumns = ['action', 'characterName', ...BASE]; + displayedColumns = ['action', 'characterName', ...BASE, ...ATTRIBUTES]; filterableAttributeGroups: FilterModel[] = [ - { attributes: BASE, label: 'ATTRIBUTES_GROUP.BASE', checked: true }, - { attributes: ATTRIBUTES, label: 'ATTRIBUTES_GROUP.ATTRIBUTES' }, - { attributes: WEAPON_SKILLS_ONE_HANDED, label: 'ATTRIBUTES_GROUP.WEAPON_SKILLS_ONE_HANDED' }, - { attributes: WEAPON_SKILLS_TWO_HANDED, label: 'ATTRIBUTES_GROUP.WEAPON_SKILLS_TWO_HANDED' }, - { attributes: WEAPON_SKILLS_RANGED, label: 'ATTRIBUTES_GROUP.WEAPON_SKILLS_RANGED' }, - { attributes: WEAPON_SKILLS_MAGIC, label: 'ATTRIBUTES_GROUP.WEAPON_SKILLS_MAGIC' }, + { attributes: BASE.concat(ATTRIBUTES), label: 'ATTRIBUTES_GROUP.ATTRIBUTES', checked: true }, + { attributes: BASE.concat(WEAPON_SKILLS_ONE_HANDED), label: 'ATTRIBUTES_GROUP.WEAPON_SKILLS_ONE_HANDED' }, + { attributes: BASE.concat(WEAPON_SKILLS_TWO_HANDED), label: 'ATTRIBUTES_GROUP.WEAPON_SKILLS_TWO_HANDED' }, + { attributes: BASE.concat(WEAPON_SKILLS_RANGED), label: 'ATTRIBUTES_GROUP.WEAPON_SKILLS_RANGED' }, + { attributes: BASE.concat(WEAPON_SKILLS_MAGIC), label: 'ATTRIBUTES_GROUP.WEAPON_SKILLS_MAGIC' }, { attributes: TRADE_SKILLS, label: 'ATTRIBUTES_GROUP.TRADE_SKILLS' }, { attributes: REFINING_SKILLS, label: 'ATTRIBUTES_GROUP.REFINING_SKILLS' }, { attributes: GATHERING_SKILLS, label: 'ATTRIBUTES_GROUP.GATHERING_SKILLS' } From bf8d186e2c213c892ef8c8323456a7703c50a36d Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Tue, 23 Nov 2021 11:34:51 +0100 Subject: [PATCH 02/25] feat: added full-calender lib and primeNG --- webapp/angular.json | 3 + webapp/package-lock.json | 236 +- webapp/package.json | 11 +- webapp/src/app/app.module.ts | 10 +- .../home-calendar/home-calendar.component.css | 5 + .../home-calendar.component.html | 8 + .../home-calendar.component.spec.ts | 25 + .../home-calendar/home-calendar.component.ts | 31 + .../home-navigation.component.css | 4 + .../home-navigation.component.html | 4 +- webapp/src/app/pages/home/home-page.module.ts | 20 +- .../home/routes/root/home.component.html | 1 + webapp/src/theme.css | 1971 ++++++----------- 13 files changed, 964 insertions(+), 1365 deletions(-) create mode 100644 webapp/src/app/pages/home/components/home-calendar/home-calendar.component.css create mode 100644 webapp/src/app/pages/home/components/home-calendar/home-calendar.component.html create mode 100644 webapp/src/app/pages/home/components/home-calendar/home-calendar.component.spec.ts create mode 100644 webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts diff --git a/webapp/angular.json b/webapp/angular.json index fd692c0..c4ac37b 100644 --- a/webapp/angular.json +++ b/webapp/angular.json @@ -37,6 +37,9 @@ "styles": [ "./src/theme.css", "./node_modules/cookieconsent/build/cookieconsent.min.css", + "./node_modules/primeng/resources/themes/md-dark-indigo/theme.css", + "./node_modules/primeng/resources/primeng.min.css", + "./node_modules/primeicons/primeicons.css", "./src/styles.css" ], "scripts": [ diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 5d01f9c..41695a1 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -18,14 +18,23 @@ "@angular/forms": "~12.2.0", "@angular/localize": "~12.2.0", "@angular/material": "^12.2.11", + "@angular/material-moment-adapter": "^12.2.13", "@angular/platform-browser": "~12.2.0", "@angular/platform-browser-dynamic": "~12.2.0", "@angular/router": "~12.2.0", + "@fullcalendar/angular": "^5.8.0", + "@fullcalendar/core": "^5.8.0", + "@fullcalendar/daygrid": "^5.8.0", + "@fullcalendar/interaction": "^5.8.0", + "@fullcalendar/timegrid": "^5.8.0", "@ngx-translate/core": "^13.0.0", "@ngx-translate/http-loader": "^6.0.0", "cookieconsent": "^3.1.1", "jsonwebtoken": "^8.5.1", + "moment": "^2.29.1", "ngx-cookieconsent": "^2.2.3", + "primeicons": "^5.0.0", + "primeng": "^12.2.2", "rxjs": "~6.6.0", "tslib": "^2.3.0", "zone.js": "~0.11.4" @@ -436,9 +445,9 @@ } }, "node_modules/@angular/cdk": { - "version": "12.2.12", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.12.tgz", - "integrity": "sha512-AK+74JZP95BDj9OJ1YUaRqPXkgs+oadTk7z+8omu1RcvDoUivouKxgODCQX5jI7rZeQIlnV49hEgBne07hYk4A==", + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.13.tgz", + "integrity": "sha512-zSKRhECyFqhingIeyRInIyTvYErt4gWo+x5DQr0b7YLUbU8DZSwWnG4w76Ke2s4U8T7ry1jpJBHoX/e8YBpGMg==", "dependencies": { "tslib": "^2.2.0" }, @@ -655,21 +664,34 @@ } }, "node_modules/@angular/material": { - "version": "12.2.12", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-12.2.12.tgz", - "integrity": "sha512-Fgpffplmd3KfoK9Ms34jYXRaqFYLbgXdGl250Acg7fV16LN1KTOyYm2Qs+FQuqyhuXfhePPt6Srh7VcGTLT4+g==", + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-12.2.13.tgz", + "integrity": "sha512-6g2GyN4qp2D+DqY2AwrQuPB3cd9gybvQVXvNRbTPXEulHr+LgGei00ySdFHFp6RvdGSMZ4i3LM1Fq3VkFxhCfQ==", "dependencies": { "tslib": "^2.2.0" }, "peerDependencies": { "@angular/animations": "^12.0.0 || ^13.0.0-0", - "@angular/cdk": "12.2.12", + "@angular/cdk": "12.2.13", "@angular/common": "^12.0.0 || ^13.0.0-0", "@angular/core": "^12.0.0 || ^13.0.0-0", "@angular/forms": "^12.0.0 || ^13.0.0-0", "rxjs": "^6.5.3 || ^7.0.0" } }, + "node_modules/@angular/material-moment-adapter": { + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-12.2.13.tgz", + "integrity": "sha512-0XhkAhBalvNvIfvi9t5qDKaTBtOuIqd1cABfotyaTecWmw57VoH0KoJtNekXjxDcx71S0LaqeYXfqTo4m+duSA==", + "dependencies": { + "tslib": "^2.2.0" + }, + "peerDependencies": { + "@angular/core": "^12.0.0 || ^13.0.0-0", + "@angular/material": "12.2.13", + "moment": "^2.18.1" + } + }, "node_modules/@angular/platform-browser": { "version": "12.2.12", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-12.2.12.tgz", @@ -2465,6 +2487,70 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@fullcalendar/angular": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/angular/-/angular-5.10.1.tgz", + "integrity": "sha512-aV2MejZMTBGzlgQ+HN2kyWThYrbf8nusr88bB6HLHixkzjWnfdxzC8NwMYz8JOatJzH9UmOiaPQRWdTU9S8WEg==", + "dependencies": { + "@fullcalendar/core": "~5.10.1", + "fast-deep-equal": "^3.1.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 6.9.0", + "npm": ">= 3.0.0" + }, + "peerDependencies": { + "@angular/common": "9 - 12", + "@angular/core": "9 - 12" + } + }, + "node_modules/@fullcalendar/common": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/common/-/common-5.10.1.tgz", + "integrity": "sha512-EumKIJcQTvQdTs75/9dmeREFgjcRVWzqHJS1Xvlz5mNsmB+w9EINCHETRjChtAQg1WD/lTQyVj4sHsKO7vCMSw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fullcalendar/core": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-5.10.1.tgz", + "integrity": "sha512-8sVuC6ywXV+cxqsqTZaR1hgUqeyjVed20NyZ7lGW9AY0kma1GIEwLgqPS5Q6uVhHyin68lmgecKfJCwhxENE8w==", + "dependencies": { + "@fullcalendar/common": "~5.10.1", + "preact": "^10.0.5", + "tslib": "^2.1.0" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-5.10.1.tgz", + "integrity": "sha512-sfUMP+rew0krsBffgNcWWKhBCiyytGfRKZJoc64E8ohX7VWjPcPZuB1xgO5U4wPLmNkT0rZiHoGeQGTXw1+ZKg==", + "dependencies": { + "@fullcalendar/common": "~5.10.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-5.10.1.tgz", + "integrity": "sha512-H1g1QeXg7yXtUcKmVtfg7uzm5R5ElFTvYniiXU+8kJda69IDg7Lee+Y7UDv5dvLb5/HxO86RhPVxRtcOQ8XdXw==", + "dependencies": { + "@fullcalendar/common": "~5.10.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-5.10.1.tgz", + "integrity": "sha512-0O0m+JzFBlg8gxYr/rIjZViRlbndCtjZlDjjIylQHFBeWC32e3cpHEavKGbTIBLN8SDilUYAJnE21abSqC2G/w==", + "dependencies": { + "@fullcalendar/common": "~5.10.1", + "@fullcalendar/daygrid": "~5.10.1", + "tslib": "^2.1.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -10161,6 +10247,14 @@ "node": ">=10" } }, + "node_modules/moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -13140,6 +13234,15 @@ "node": ">=6.14.4" } }, + "node_modules/preact": { + "version": "10.5.15", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.15.tgz", + "integrity": "sha512-5chK29n6QcJc3m1lVrKQSQ+V7K1Gb8HeQY6FViQ5AxCAEGu3DaHffWNDkC9+miZgsLvbvU9rxbV1qinGHMHzqA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -13184,6 +13287,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/primeicons": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-5.0.0.tgz", + "integrity": "sha512-heygWF0X5HFI1otlZE62pp6ye7sZ8om78J9au2BRkg8O7Y8AHTZ9qKMRzchZUHLe8zUAvdi6hZzzm9XxgwIExw==" + }, + "node_modules/primeng": { + "version": "12.2.2", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-12.2.2.tgz", + "integrity": "sha512-PB2kZzwhAb3M5nQXe1hUfsBGXAbhGcf/NVwg8mp4P5JETJ4nAGgixNPPGjBdmFW6l8QzmlsAVb3nMF+rPaZH4g==", + "dependencies": { + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@angular/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "@angular/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "@angular/forms": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "rxjs": "^6.0.0", + "zone.js": "^0.10.2 || ^0.11.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -17008,9 +17131,9 @@ } }, "@angular/cdk": { - "version": "12.2.12", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.12.tgz", - "integrity": "sha512-AK+74JZP95BDj9OJ1YUaRqPXkgs+oadTk7z+8omu1RcvDoUivouKxgODCQX5jI7rZeQIlnV49hEgBne07hYk4A==", + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-12.2.13.tgz", + "integrity": "sha512-zSKRhECyFqhingIeyRInIyTvYErt4gWo+x5DQr0b7YLUbU8DZSwWnG4w76Ke2s4U8T7ry1jpJBHoX/e8YBpGMg==", "requires": { "parse5": "^5.0.0", "tslib": "^2.2.0" @@ -17148,9 +17271,17 @@ } }, "@angular/material": { - "version": "12.2.12", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-12.2.12.tgz", - "integrity": "sha512-Fgpffplmd3KfoK9Ms34jYXRaqFYLbgXdGl250Acg7fV16LN1KTOyYm2Qs+FQuqyhuXfhePPt6Srh7VcGTLT4+g==", + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-12.2.13.tgz", + "integrity": "sha512-6g2GyN4qp2D+DqY2AwrQuPB3cd9gybvQVXvNRbTPXEulHr+LgGei00ySdFHFp6RvdGSMZ4i3LM1Fq3VkFxhCfQ==", + "requires": { + "tslib": "^2.2.0" + } + }, + "@angular/material-moment-adapter": { + "version": "12.2.13", + "resolved": "https://registry.npmjs.org/@angular/material-moment-adapter/-/material-moment-adapter-12.2.13.tgz", + "integrity": "sha512-0XhkAhBalvNvIfvi9t5qDKaTBtOuIqd1cABfotyaTecWmw57VoH0KoJtNekXjxDcx71S0LaqeYXfqTo4m+duSA==", "requires": { "tslib": "^2.2.0" } @@ -18396,6 +18527,62 @@ } } }, + "@fullcalendar/angular": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/angular/-/angular-5.10.1.tgz", + "integrity": "sha512-aV2MejZMTBGzlgQ+HN2kyWThYrbf8nusr88bB6HLHixkzjWnfdxzC8NwMYz8JOatJzH9UmOiaPQRWdTU9S8WEg==", + "requires": { + "@fullcalendar/core": "~5.10.1", + "fast-deep-equal": "^3.1.1", + "tslib": "^2.0.0" + } + }, + "@fullcalendar/common": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/common/-/common-5.10.1.tgz", + "integrity": "sha512-EumKIJcQTvQdTs75/9dmeREFgjcRVWzqHJS1Xvlz5mNsmB+w9EINCHETRjChtAQg1WD/lTQyVj4sHsKO7vCMSw==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@fullcalendar/core": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-5.10.1.tgz", + "integrity": "sha512-8sVuC6ywXV+cxqsqTZaR1hgUqeyjVed20NyZ7lGW9AY0kma1GIEwLgqPS5Q6uVhHyin68lmgecKfJCwhxENE8w==", + "requires": { + "@fullcalendar/common": "~5.10.1", + "preact": "^10.0.5", + "tslib": "^2.1.0" + } + }, + "@fullcalendar/daygrid": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-5.10.1.tgz", + "integrity": "sha512-sfUMP+rew0krsBffgNcWWKhBCiyytGfRKZJoc64E8ohX7VWjPcPZuB1xgO5U4wPLmNkT0rZiHoGeQGTXw1+ZKg==", + "requires": { + "@fullcalendar/common": "~5.10.1", + "tslib": "^2.1.0" + } + }, + "@fullcalendar/interaction": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-5.10.1.tgz", + "integrity": "sha512-H1g1QeXg7yXtUcKmVtfg7uzm5R5ElFTvYniiXU+8kJda69IDg7Lee+Y7UDv5dvLb5/HxO86RhPVxRtcOQ8XdXw==", + "requires": { + "@fullcalendar/common": "~5.10.1", + "tslib": "^2.1.0" + } + }, + "@fullcalendar/timegrid": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-5.10.1.tgz", + "integrity": "sha512-0O0m+JzFBlg8gxYr/rIjZViRlbndCtjZlDjjIylQHFBeWC32e3cpHEavKGbTIBLN8SDilUYAJnE21abSqC2G/w==", + "requires": { + "@fullcalendar/common": "~5.10.1", + "@fullcalendar/daygrid": "~5.10.1", + "tslib": "^2.1.0" + } + }, "@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -24252,6 +24439,11 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -26415,6 +26607,11 @@ "uniq": "^1.0.1" } }, + "preact": { + "version": "10.5.15", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.15.tgz", + "integrity": "sha512-5chK29n6QcJc3m1lVrKQSQ+V7K1Gb8HeQY6FViQ5AxCAEGu3DaHffWNDkC9+miZgsLvbvU9rxbV1qinGHMHzqA==" + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -26441,6 +26638,19 @@ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" }, + "primeicons": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-5.0.0.tgz", + "integrity": "sha512-heygWF0X5HFI1otlZE62pp6ye7sZ8om78J9au2BRkg8O7Y8AHTZ9qKMRzchZUHLe8zUAvdi6hZzzm9XxgwIExw==" + }, + "primeng": { + "version": "12.2.2", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-12.2.2.tgz", + "integrity": "sha512-PB2kZzwhAb3M5nQXe1hUfsBGXAbhGcf/NVwg8mp4P5JETJ4nAGgixNPPGjBdmFW6l8QzmlsAVb3nMF+rPaZH4g==", + "requires": { + "tslib": "^2.1.0" + } + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/webapp/package.json b/webapp/package.json index b270a80..a2e0233 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -37,14 +37,23 @@ "@angular/forms": "~12.2.0", "@angular/localize": "~12.2.0", "@angular/material": "^12.2.11", + "@angular/material-moment-adapter": "^12.2.13", "@angular/platform-browser": "~12.2.0", "@angular/platform-browser-dynamic": "~12.2.0", "@angular/router": "~12.2.0", + "@fullcalendar/angular": "^5.8.0", + "@fullcalendar/core": "^5.8.0", + "@fullcalendar/daygrid": "^5.8.0", + "@fullcalendar/interaction": "^5.8.0", + "@fullcalendar/timegrid": "^5.8.0", "@ngx-translate/core": "^13.0.0", "@ngx-translate/http-loader": "^6.0.0", "cookieconsent": "^3.1.1", "jsonwebtoken": "^8.5.1", + "moment": "^2.29.1", "ngx-cookieconsent": "^2.2.3", + "primeicons": "^5.0.0", + "primeng": "^12.2.2", "rxjs": "~6.6.0", "tslib": "^2.3.0", "zone.js": "~0.11.4" @@ -79,4 +88,4 @@ "prettier": "^2.4.1", "typescript": "~4.3.5" } -} \ No newline at end of file +} diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts index fd93629..8301551 100644 --- a/webapp/src/app/app.module.ts +++ b/webapp/src/app/app.module.ts @@ -26,6 +26,11 @@ import { AdminModule } from './services/admin/admin.module'; import { CharacterModule } from './services/character/character.module'; import { SnackbarModule } from './services/snackbar/snackbar.module'; import { InterceptorModule } from './interceptor/interceptor.module'; +import { MatMomentDateModule } from '@angular/material-moment-adapter'; +import { FullCalendarModule } from '@fullcalendar/angular'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import interactionPlugin from '@fullcalendar/interaction'; const cookieConfig: NgcCookieConsentConfig = { cookie: { @@ -56,6 +61,8 @@ const i18nConfig: TranslateModuleConfig = { defaultLanguage: 'en' }; +FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPlugin]); + @NgModule({ declarations: [AppComponent, HeaderComponent, FooterComponent], imports: [ @@ -80,7 +87,8 @@ const i18nConfig: TranslateModuleConfig = { MatToolbarModule, MatMenuModule, MatIconModule, - MatButtonModule + MatButtonModule, + MatMomentDateModule ], providers: [AppComponent, LoginGuard, AdminGuard], bootstrap: [AppComponent], diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.css b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.css new file mode 100644 index 0000000..b4f3dac --- /dev/null +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.css @@ -0,0 +1,5 @@ +.calendar-container { + width: 1024px; +} + + diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.html b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.html new file mode 100644 index 0000000..5599a58 --- /dev/null +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.html @@ -0,0 +1,8 @@ +
+ +
+ +
+
+
+ diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.spec.ts b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.spec.ts new file mode 100644 index 0000000..684d2ac --- /dev/null +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeCalendarComponent } from './home-calendar.component'; + +describe('HomeCalendarComponent', () => { + let component: HomeCalendarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HomeCalendarComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeCalendarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts new file mode 100644 index 0000000..bf12c08 --- /dev/null +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { CalendarOptions } from '@fullcalendar/angular'; +import * as moment from 'moment'; + +@Component({ + selector: 'app-home-calendar', + templateUrl: './home-calendar.component.html', + styleUrls: ['./home-calendar.component.css'] +}) +export class HomeCalendarComponent { + options: CalendarOptions = { + initialDate: moment().format('yyyy-MM-DD'), + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,timeGridWeek,timeGridDay' + }, + footerToolbar: { + left: '', + center: '', + right: '' + }, + aspectRatio: 2, + editable: true, + selectable: true, + selectMirror: true, + dayMaxEvents: true, + firstDay: 1, + locale: navigator.language + }; +} diff --git a/webapp/src/app/pages/home/components/home-navigation/home-navigation.component.css b/webapp/src/app/pages/home/components/home-navigation/home-navigation.component.css index 5a2ce07..cbcfbc4 100644 --- a/webapp/src/app/pages/home/components/home-navigation/home-navigation.component.css +++ b/webapp/src/app/pages/home/components/home-navigation/home-navigation.component.css @@ -7,6 +7,10 @@ margin: 6px 6px 6px 6px; } +.home-card-container { + flex: 0 0 150px; +} + .home-card:hover { cursor: pointer; } diff --git a/webapp/src/app/pages/home/components/home-navigation/home-navigation.component.html b/webapp/src/app/pages/home/components/home-navigation/home-navigation.component.html index 9ca4cd1..932a97a 100644 --- a/webapp/src/app/pages/home/components/home-navigation/home-navigation.component.html +++ b/webapp/src/app/pages/home/components/home-navigation/home-navigation.component.html @@ -1,6 +1,6 @@ -
+
-
+
diff --git a/webapp/src/app/pages/home/home-page.module.ts b/webapp/src/app/pages/home/home-page.module.ts index 0fe951a..be8b4e5 100644 --- a/webapp/src/app/pages/home/home-page.module.ts +++ b/webapp/src/app/pages/home/home-page.module.ts @@ -6,10 +6,26 @@ import { HomeComponent } from './routes/root/home.component'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { CommonModule } from '@angular/common'; +import { HomeCalendarComponent } from './components/home-calendar/home-calendar.component'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { CalendarModule } from 'primeng/calendar'; +import { FullCalendarModule } from '@fullcalendar/angular'; +import { FullCalendarModule as NgFullCalendarModule } from 'primeng/fullcalendar'; @NgModule({ - imports: [HomeRoutingModule, NavigationModule, MatCardModule, MatIconModule, CommonModule], - declarations: [HomeComponent, HomeNavigationComponent], + imports: [ + HomeRoutingModule, + NavigationModule, + MatCardModule, + MatIconModule, + CommonModule, + MatDatepickerModule, + FullCalendarModule, + CalendarModule, + FullCalendarModule, + NgFullCalendarModule + ], + declarations: [HomeComponent, HomeNavigationComponent, HomeCalendarComponent], providers: [], exports: [] }) diff --git a/webapp/src/app/pages/home/routes/root/home.component.html b/webapp/src/app/pages/home/routes/root/home.component.html index 5a893d8..8cdc716 100644 --- a/webapp/src/app/pages/home/routes/root/home.component.html +++ b/webapp/src/app/pages/home/routes/root/home.component.html @@ -1 +1,2 @@ + diff --git a/webapp/src/theme.css b/webapp/src/theme.css index 336581c..f0bc62c 100644 --- a/webapp/src/theme.css +++ b/webapp/src/theme.css @@ -1,410 +1,329 @@ +/** +* Generated theme by Material Theme Generator +* https://materialtheme.arcsine.dev +* Fork at: https://materialtheme.arcsine.dev/?c=YHBhbGV0dGU$YHByaW1hcnk$YF48IzlmYThkYSIsIj9lcjwjZTJlNWY0IiwiO2VyPCM4MzhkY2J$LCIlPmBePCM3OTc5NzkiLCI~ZXI8I2Q3ZDdkNyIsIjtlcjwjNWM1YzVjfiwid2Fybj5gXjwjZmYwMDAwIiwiP2VyPCNmZmIzYjMiLCI7ZXI8I2ZmMDAwMH4sIj9UZXh0PCMxMjEyMTIiLCI~PTwjZmFmYWZhIiwiO1RleHQ8I2ZmZmZmZiIsIjs9PCMxZTFlMWV$LCJmb250cz5bYEA8KC00fixgQDwoLTN$LGBAPCgtMn4sYEA8KC0xfixgQDxoZWFkbGluZX4sYEA8dGl0bGV$LGBAPHN1YiktMn4sYEA8c3ViKS0xfixgQDxib2R5LTJ$LGBAPGJvZHktMX4sYEA8YnV0dG9ufixgQDxjYXB0aW9ufixgQDxpbnB1dCIsInNpemU$bnVsbH1dLCJpY29uczxGaWxsZWQiLCI~bmVzcz5mYWxzZSwidmVyc2lvbj4xMX0= +*/ @import 'https://fonts.googleapis.com/css?family=Material+Icons'; @import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500"); - .mat-badge-content { font-weight: 600; - font-size: 12px; -} + font-size: 12px; } .mat-badge-small .mat-badge-content { - font-size: 9px; -} + font-size: 9px; } .mat-badge-large .mat-badge-content { - font-size: 24px; -} + font-size: 24px; } .mat-h1, .mat-headline, .mat-typography h1 { font: 400 24px/32px Roboto; letter-spacing: 0em; - margin: 0 0 16px; -} + margin: 0 0 16px; } .mat-h2, .mat-title, .mat-typography h2 { font: 500 20px/32px Roboto; letter-spacing: 0.0075em; - margin: 0 0 16px; -} + margin: 0 0 16px; } .mat-h3, .mat-subheading-2, .mat-typography h3 { font: 400 16px/28px Roboto; letter-spacing: 0.0094em; - margin: 0 0 16px; -} + margin: 0 0 16px; } .mat-h4, .mat-subheading-1, .mat-typography h4 { font: 500 15px/24px Roboto; letter-spacing: 0.0067em; - margin: 0 0 16px; -} + margin: 0 0 16px; } .mat-h5, .mat-typography h5 { font: 400 calc(14px * 0.83)/20px Roboto; - margin: 0 0 12px; -} + margin: 0 0 12px; } .mat-h6, .mat-typography h6 { font: 400 calc(14px * 0.67)/20px Roboto; - margin: 0 0 12px; -} + margin: 0 0 12px; } .mat-body-strong, .mat-body-2 { font: 500 14px/24px Roboto; - letter-spacing: 0.0179em; -} + letter-spacing: 0.0179em; } .mat-body, .mat-body-1, .mat-typography { font: 400 14px/20px Roboto; - letter-spacing: 0.0179em; -} - + letter-spacing: 0.0179em; } .mat-body p, .mat-body-1 p, .mat-typography p { - margin: 0 0 12px; -} + margin: 0 0 12px; } .mat-small, .mat-caption { font: 400 12px/20px Roboto; - letter-spacing: 0.0333em; -} + letter-spacing: 0.0333em; } .mat-display-4, .mat-typography .mat-display-4 { font: 300 112px/112px Roboto; letter-spacing: -0.0134em; - margin: 0 0 56px; -} + margin: 0 0 56px; } .mat-display-3, .mat-typography .mat-display-3 { font: 400 56px/56px Roboto; letter-spacing: -0.0089em; - margin: 0 0 64px; -} + margin: 0 0 64px; } .mat-display-2, .mat-typography .mat-display-2 { font: 400 45px/48px Roboto; letter-spacing: 0em; - margin: 0 0 64px; -} + margin: 0 0 64px; } .mat-display-1, .mat-typography .mat-display-1 { font: 400 34px/40px Roboto; letter-spacing: 0.0074em; - margin: 0 0 64px; -} + margin: 0 0 64px; } .mat-bottom-sheet-container { font: 400 14px/20px Roboto; - letter-spacing: 0.0179em; -} + letter-spacing: 0.0179em; } .mat-button, .mat-raised-button, .mat-icon-button, .mat-stroked-button, .mat-flat-button, .mat-fab, .mat-mini-fab { font-family: Roboto; font-size: 14px; - font-weight: 500; -} + font-weight: 500; } .mat-card-title { font-size: 24px; - font-weight: 500; -} + font-weight: 500; } .mat-card-header .mat-card-title { - font-size: 20px; -} + font-size: 20px; } .mat-card-subtitle, .mat-card-content { - font-size: 14px; -} + font-size: 14px; } .mat-checkbox-layout .mat-checkbox-label { - line-height: 24px; -} + line-height: 24px; } .mat-chip { font-size: 14px; - font-weight: 500; -} - + font-weight: 500; } .mat-chip .mat-chip-trailing-icon.mat-icon, .mat-chip .mat-chip-remove.mat-icon { - font-size: 18px; -} + font-size: 18px; } .mat-header-cell { font-size: 12px; - font-weight: 500; -} + font-weight: 500; } .mat-cell, .mat-footer-cell { - font-size: 14px; -} + font-size: 14px; } .mat-calendar-body { - font-size: 13px; -} + font-size: 13px; } .mat-calendar-body-label, .mat-calendar-period-button { font-size: 14px; - font-weight: 500; -} + font-weight: 500; } .mat-calendar-table-header th { font-size: 11px; - font-weight: 400; -} + font-weight: 400; } .mat-dialog-title { font: 500 20px/32px Roboto; - letter-spacing: 0.0075em; -} + letter-spacing: 0.0075em; } .mat-expansion-panel-header { font-family: Roboto; font-size: 15px; - font-weight: 500; -} + font-weight: 500; } .mat-expansion-panel-content { font: 400 14px/20px Roboto; - letter-spacing: 0.0179em; -} + letter-spacing: 0.0179em; } .mat-form-field { font-size: inherit; font-weight: 400; line-height: 1.125; font-family: Roboto; - letter-spacing: 1.5px; -} + letter-spacing: 1.5px; } .mat-form-field-wrapper { - padding-bottom: 1.34375em; -} + padding-bottom: 1.34375em; } .mat-form-field-prefix .mat-icon, .mat-form-field-suffix .mat-icon { font-size: 150%; - line-height: 1.125; -} + line-height: 1.125; } .mat-form-field-prefix .mat-icon-button, .mat-form-field-suffix .mat-icon-button { height: 1.5em; - width: 1.5em; -} - + width: 1.5em; } .mat-form-field-prefix .mat-icon-button .mat-icon, .mat-form-field-suffix .mat-icon-button .mat-icon { height: 1.125em; - line-height: 1.125; -} + line-height: 1.125; } .mat-form-field-infix { padding: 0.5em 0; - border-top: 0.84375em solid transparent; -} + border-top: 0.84375em solid transparent; } .mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label, .mat-form-field-can-float .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { transform: translateY(-1.34375em) scale(0.75); - width: 133.3333333333%; -} + width: 133.3333333333%; } .mat-form-field-can-float .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper .mat-form-field-label { transform: translateY(-1.34374em) scale(0.75); - width: 133.3333433333%; -} + width: 133.3333433333%; } .mat-form-field-label-wrapper { top: -0.84375em; - padding-top: 0.84375em; -} + padding-top: 0.84375em; } .mat-form-field-label { - top: 1.34375em; -} + top: 1.34375em; } .mat-form-field-underline { - bottom: 1.34375em; -} + bottom: 1.34375em; } .mat-form-field-subscript-wrapper { font-size: 75%; margin-top: 0.6666666667em; - top: calc(100% - 1.7916666667em); -} + top: calc(100% - 1.7916666667em); } .mat-form-field-appearance-legacy .mat-form-field-wrapper { - padding-bottom: 1.25em; -} + padding-bottom: 1.25em; } .mat-form-field-appearance-legacy .mat-form-field-infix { - padding: 0.4375em 0; -} + padding: 0.4375em 0; } .mat-form-field-appearance-legacy.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label, .mat-form-field-appearance-legacy.mat-form-field-can-float .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { transform: translateY(-1.28125em) scale(0.75) perspective(100px) translateZ(0.001px); -ms-transform: translateY(-1.28125em) scale(0.75); - width: 133.3333333333%; -} + width: 133.3333333333%; } .mat-form-field-appearance-legacy.mat-form-field-can-float .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper .mat-form-field-label { transform: translateY(-1.28125em) scale(0.75) perspective(100px) translateZ(0.00101px); -ms-transform: translateY(-1.28124em) scale(0.75); - width: 133.3333433333%; -} + width: 133.3333433333%; } .mat-form-field-appearance-legacy.mat-form-field-can-float .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper .mat-form-field-label { transform: translateY(-1.28125em) scale(0.75) perspective(100px) translateZ(0.00102px); -ms-transform: translateY(-1.28123em) scale(0.75); - width: 133.3333533333%; -} + width: 133.3333533333%; } .mat-form-field-appearance-legacy .mat-form-field-label { - top: 1.28125em; -} + top: 1.28125em; } .mat-form-field-appearance-legacy .mat-form-field-underline { - bottom: 1.25em; -} + bottom: 1.25em; } .mat-form-field-appearance-legacy .mat-form-field-subscript-wrapper { margin-top: 0.5416666667em; - top: calc(100% - 1.6666666667em); -} + top: calc(100% - 1.6666666667em); } @media print { .mat-form-field-appearance-legacy.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label, .mat-form-field-appearance-legacy.mat-form-field-can-float .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { - transform: translateY(-1.28122em) scale(0.75); - } - + transform: translateY(-1.28122em) scale(0.75); } .mat-form-field-appearance-legacy.mat-form-field-can-float .mat-form-field-autofill-control:-webkit-autofill + .mat-form-field-label-wrapper .mat-form-field-label { - transform: translateY(-1.28121em) scale(0.75); - } - + transform: translateY(-1.28121em) scale(0.75); } .mat-form-field-appearance-legacy.mat-form-field-can-float .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper .mat-form-field-label { - transform: translateY(-1.2812em) scale(0.75); - } -} + transform: translateY(-1.2812em) scale(0.75); } } .mat-form-field-appearance-fill .mat-form-field-infix { - padding: 0.25em 0 0.75em 0; -} + padding: 0.25em 0 0.75em 0; } .mat-form-field-appearance-fill .mat-form-field-label { top: 1.09375em; - margin-top: -0.5em; -} + margin-top: -0.5em; } .mat-form-field-appearance-fill.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label, .mat-form-field-appearance-fill.mat-form-field-can-float .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { transform: translateY(-0.59375em) scale(0.75); - width: 133.3333333333%; -} + width: 133.3333333333%; } .mat-form-field-appearance-fill.mat-form-field-can-float .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper .mat-form-field-label { transform: translateY(-0.59374em) scale(0.75); - width: 133.3333433333%; -} + width: 133.3333433333%; } .mat-form-field-appearance-outline .mat-form-field-infix { - padding: 1em 0 1em 0; -} + padding: 1em 0 1em 0; } .mat-form-field-appearance-outline .mat-form-field-label { top: 1.84375em; - margin-top: -0.25em; -} + margin-top: -0.25em; } .mat-form-field-appearance-outline.mat-form-field-can-float.mat-form-field-should-float .mat-form-field-label, .mat-form-field-appearance-outline.mat-form-field-can-float .mat-input-server:focus + .mat-form-field-label-wrapper .mat-form-field-label { transform: translateY(-1.59375em) scale(0.75); - width: 133.3333333333%; -} + width: 133.3333333333%; } .mat-form-field-appearance-outline.mat-form-field-can-float .mat-input-server[label]:not(:label-shown) + .mat-form-field-label-wrapper .mat-form-field-label { transform: translateY(-1.59374em) scale(0.75); - width: 133.3333433333%; -} + width: 133.3333433333%; } .mat-grid-tile-header, .mat-grid-tile-footer { - font-size: 14px; -} - + font-size: 14px; } .mat-grid-tile-header .mat-line, .mat-grid-tile-footer .mat-line { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; - box-sizing: border-box; -} - + box-sizing: border-box; } .mat-grid-tile-header .mat-line:nth-child(n+2), .mat-grid-tile-footer .mat-line:nth-child(n+2) { - font-size: 12px; -} + font-size: 12px; } input.mat-input-element { - margin-top: -0.0625em; -} + margin-top: -0.0625em; } .mat-menu-item { font-family: Roboto; font-size: 14px; - font-weight: 400; -} + font-weight: 400; } .mat-paginator, .mat-paginator-page-size .mat-select-trigger { font-family: Roboto; - font-size: 12px; -} + font-size: 12px; } .mat-select-trigger { - height: 1.125em; -} + height: 1.125em; } .mat-slider-thumb-label-text { font-size: 12px; - font-weight: 500; -} + font-weight: 500; } .mat-step-label { font-size: 14px; - font-weight: 400; -} + font-weight: 400; } .mat-step-sub-label-error { - font-weight: normal; -} + font-weight: normal; } .mat-step-label-error { - font-size: 14px; -} + font-size: 14px; } .mat-step-label-selected { font-size: 14px; - font-weight: 500; -} + font-weight: 500; } .mat-tab-label, .mat-tab-link { font-family: Roboto; font-size: 14px; - font-weight: 500; -} + font-weight: 500; } .mat-toolbar, .mat-toolbar h1, @@ -415,147 +334,110 @@ input.mat-input-element { .mat-toolbar h6 { font: 500 20px/32px Roboto; letter-spacing: 0.0075em; - margin: 0; -} + margin: 0; } .mat-tooltip { font-size: 10px; padding-top: 6px; - padding-bottom: 6px; -} + padding-bottom: 6px; } .mat-tooltip-handset { font-size: 14px; padding-top: 8px; - padding-bottom: 8px; -} + padding-bottom: 8px; } .mat-list-base .mat-list-item { - font-size: 16px; -} - + font-size: 16px; } .mat-list-base .mat-list-item .mat-line { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; - box-sizing: border-box; -} - + box-sizing: border-box; } .mat-list-base .mat-list-item .mat-line:nth-child(n+2) { - font-size: 14px; -} + font-size: 14px; } .mat-list-base .mat-list-option { - font-size: 16px; -} - + font-size: 16px; } .mat-list-base .mat-list-option .mat-line { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; - box-sizing: border-box; -} - + box-sizing: border-box; } .mat-list-base .mat-list-option .mat-line:nth-child(n+2) { - font-size: 14px; -} + font-size: 14px; } .mat-list-base .mat-subheader { font-family: Roboto; font-size: 14px; - font-weight: 500; -} + font-weight: 500; } .mat-list-base[dense] .mat-list-item { - font-size: 12px; -} - + font-size: 12px; } .mat-list-base[dense] .mat-list-item .mat-line { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; - box-sizing: border-box; -} - + box-sizing: border-box; } .mat-list-base[dense] .mat-list-item .mat-line:nth-child(n+2) { - font-size: 12px; -} + font-size: 12px; } .mat-list-base[dense] .mat-list-option { - font-size: 12px; -} - + font-size: 12px; } .mat-list-base[dense] .mat-list-option .mat-line { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; - box-sizing: border-box; -} - + box-sizing: border-box; } .mat-list-base[dense] .mat-list-option .mat-line:nth-child(n+2) { - font-size: 12px; -} + font-size: 12px; } .mat-list-base[dense] .mat-subheader { font-size: 12px; - font-weight: 500; -} + font-weight: 500; } .mat-option { - font-size: 16px; -} + font-size: 16px; } .mat-optgroup-label { font: 500 14px/24px Roboto; - letter-spacing: 0.0179em; -} + letter-spacing: 0.0179em; } .mat-simple-snackbar { font-family: Roboto; - font-size: 14px; -} + font-size: 14px; } .mat-simple-snackbar-action { line-height: 1; font-family: inherit; font-size: inherit; - font-weight: 500; -} + font-weight: 500; } .mat-tree-node, .mat-nested-tree-node { font-weight: 400; - font-size: 14px; -} + font-size: 14px; } .mat-ripple { overflow: hidden; - position: relative; -} - + position: relative; } .mat-ripple:not(:empty) { - transform: translateZ(0); -} + transform: translateZ(0); } .mat-ripple.mat-ripple-unbounded { - overflow: visible; -} + overflow: visible; } .mat-ripple-element { position: absolute; border-radius: 50%; pointer-events: none; transition: opacity, transform 0ms cubic-bezier(0, 0, 0.2, 1); - transform: scale(0); -} - + transform: scale(0); } .cdk-high-contrast-active .mat-ripple-element { - display: none; -} + display: none; } .cdk-visually-hidden { border: 0; @@ -569,31 +451,25 @@ input.mat-input-element { white-space: nowrap; outline: 0; -webkit-appearance: none; - -moz-appearance: none; -} + -moz-appearance: none; } .cdk-overlay-container, .cdk-global-overlay-wrapper { pointer-events: none; top: 0; left: 0; height: 100%; - width: 100%; -} + width: 100%; } .cdk-overlay-container { position: fixed; - z-index: 1000; -} - + z-index: 1000; } .cdk-overlay-container:empty { - display: none; -} + display: none; } .cdk-global-overlay-wrapper { display: flex; position: absolute; - z-index: 1000; -} + z-index: 1000; } .cdk-overlay-pane { position: absolute; @@ -602,8 +478,7 @@ input.mat-input-element { z-index: 1000; display: flex; max-width: 100%; - max-height: 100%; -} + max-height: 100%; } .cdk-overlay-backdrop { position: absolute; @@ -615,24 +490,17 @@ input.mat-input-element { pointer-events: auto; -webkit-tap-highlight-color: transparent; transition: opacity 400ms cubic-bezier(0.25, 0.8, 0.25, 1); - opacity: 0; -} - + opacity: 0; } .cdk-overlay-backdrop.cdk-overlay-backdrop-showing { - opacity: 1; -} - + opacity: 1; } .cdk-high-contrast-active .cdk-overlay-backdrop.cdk-overlay-backdrop-showing { - opacity: 0.6; -} + opacity: 0.6; } .cdk-overlay-dark-backdrop { - background: rgba(0, 0, 0, 0.32); -} + background: rgba(0, 0, 0, 0.32); } .cdk-overlay-transparent-backdrop, .cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing { - opacity: 0; -} + opacity: 0; } .cdk-overlay-connected-position-bounding-box { position: absolute; @@ -640,318 +508,235 @@ input.mat-input-element { display: flex; flex-direction: column; min-width: 1px; - min-height: 1px; -} + min-height: 1px; } .cdk-global-scrollblock { position: fixed; width: 100%; - overflow-y: scroll; -} + overflow-y: scroll; } -@keyframes cdk-text-field-autofill-start { -} - -@keyframes cdk-text-field-autofill-end { -} - -.cdk-text-field-autofill-monitored:-webkit-autofill { - animation: cdk-text-field-autofill-start 0s 1ms; -} +@keyframes cdk-text-field-autofill-start {}@keyframes cdk-text-field-autofill-end {}.cdk-text-field-autofill-monitored:-webkit-autofill { + animation: cdk-text-field-autofill-start 0s 1ms; } .cdk-text-field-autofill-monitored:not(:-webkit-autofill) { - animation: cdk-text-field-autofill-end 0s 1ms; -} + animation: cdk-text-field-autofill-end 0s 1ms; } textarea.cdk-textarea-autosize { - resize: none; -} + resize: none; } textarea.cdk-textarea-autosize-measuring { padding: 2px 0 !important; box-sizing: content-box !important; height: auto !important; - overflow: hidden !important; -} + overflow: hidden !important; } textarea.cdk-textarea-autosize-measuring-firefox { padding: 2px 0 !important; box-sizing: content-box !important; - height: 0 !important; -} + height: 0 !important; } .mat-focus-indicator { - position: relative; -} + position: relative; } .mat-mdc-focus-indicator { - position: relative; -} + position: relative; } body { - --primary-color: #3358cc; - --primary-lighter-color: #c2cdf0; - --primary-darker-color: #203db9; - --text-primary-color: #ffffff; - --text-primary-lighter-color: rgba(0, 0, 0, 0.87); - --text-primary-darker-color: #ffffff; -} + --primary-color: #9fa8da; + --primary-lighter-color: #e2e5f4; + --primary-darker-color: #838dcb; + --text-primary-color: rgba(18, 18, 18, 0.87); + --text-primary-lighter-color: rgba(18, 18, 18, 0.87); + --text-primary-darker-color: rgba(18, 18, 18, 0.87); } body { --accent-color: #797979; --accent-lighter-color: #d7d7d7; --accent-darker-color: #5c5c5c; --text-accent-color: #ffffff; - --text-accent-lighter-color: rgba(0, 0, 0, 0.87); - --text-accent-darker-color: #ffffff; -} + --text-accent-lighter-color: rgba(18, 18, 18, 0.87); + --text-accent-darker-color: #ffffff; } body { --warn-color: #ff0000; --warn-lighter-color: #ffb3b3; --warn-darker-color: #ff0000; --text-warn-color: #ffffff; - --text-warn-lighter-color: rgba(0, 0, 0, 0.87); - --text-warn-darker-color: #ffffff; -} + --text-warn-lighter-color: rgba(18, 18, 18, 0.87); + --text-warn-darker-color: #ffffff; } .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.1); -} + background-color: rgba(255, 255, 255, 0.1); } .mat-option { - color: white; -} - + color: white; } .mat-option:hover:not(.mat-option-disabled), .mat-option:focus:not(.mat-option-disabled) { - background: rgba(250, 250, 250, 0.04); -} - + background: rgba(250, 250, 250, 0.04); } .mat-option.mat-selected:not(.mat-option-multiple):not(.mat-option-disabled) { - background: rgba(250, 250, 250, 0.04); -} - + background: rgba(250, 250, 250, 0.04); } .mat-option.mat-active { background: rgba(250, 250, 250, 0.04); - color: white; -} - + color: white; } .mat-option.mat-option-disabled { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-primary .mat-option.mat-selected:not(.mat-option-disabled) { - color: #3358cc; -} + color: #9fa8da; } .mat-accent .mat-option.mat-selected:not(.mat-option-disabled) { - color: #797979; -} + color: #797979; } .mat-warn .mat-option.mat-selected:not(.mat-option-disabled) { - color: red; -} + color: red; } .mat-optgroup-label { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-optgroup-disabled .mat-optgroup-label { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-pseudo-checkbox { - color: rgba(255, 255, 255, 0.7); -} - + color: rgba(255, 255, 255, 0.7); } .mat-pseudo-checkbox::after { - color: #2c2c2c; -} + color: #1e1e1e; } .mat-pseudo-checkbox-disabled { - color: #686868; -} + color: #686868; } .mat-primary .mat-pseudo-checkbox-checked, .mat-primary .mat-pseudo-checkbox-indeterminate { - background: #3358cc; -} + background: #9fa8da; } .mat-pseudo-checkbox-checked, .mat-pseudo-checkbox-indeterminate, .mat-accent .mat-pseudo-checkbox-checked, .mat-accent .mat-pseudo-checkbox-indeterminate { - background: #797979; -} + background: #797979; } .mat-warn .mat-pseudo-checkbox-checked, .mat-warn .mat-pseudo-checkbox-indeterminate { - background: red; -} + background: red; } .mat-pseudo-checkbox-checked.mat-pseudo-checkbox-disabled, .mat-pseudo-checkbox-indeterminate.mat-pseudo-checkbox-disabled { - background: #686868; -} + background: #686868; } .mat-app-background { - background-color: #2c2c2c; - color: white; -} + background-color: #1e1e1e; + color: white; } .mat-elevation-z0 { - box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); } .mat-elevation-z1 { - box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); } .mat-elevation-z2 { - box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); } .mat-elevation-z3 { - box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 0px 1px 8px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 0px 1px 8px 0px rgba(0, 0, 0, 0.12); } .mat-elevation-z4 { - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); } .mat-elevation-z5 { - box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 5px 8px 0px rgba(0, 0, 0, 0.14), 0px 1px 14px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 5px 8px 0px rgba(0, 0, 0, 0.14), 0px 1px 14px 0px rgba(0, 0, 0, 0.12); } .mat-elevation-z6 { - box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); } .mat-elevation-z7 { - box-shadow: 0px 4px 5px -2px rgba(0, 0, 0, 0.2), 0px 7px 10px 1px rgba(0, 0, 0, 0.14), 0px 2px 16px 1px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 4px 5px -2px rgba(0, 0, 0, 0.2), 0px 7px 10px 1px rgba(0, 0, 0, 0.14), 0px 2px 16px 1px rgba(0, 0, 0, 0.12); } .mat-elevation-z8 { - box-shadow: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12); } .mat-elevation-z9 { - box-shadow: 0px 5px 6px -3px rgba(0, 0, 0, 0.2), 0px 9px 12px 1px rgba(0, 0, 0, 0.14), 0px 3px 16px 2px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 5px 6px -3px rgba(0, 0, 0, 0.2), 0px 9px 12px 1px rgba(0, 0, 0, 0.14), 0px 3px 16px 2px rgba(0, 0, 0, 0.12); } .mat-elevation-z10 { - box-shadow: 0px 6px 6px -3px rgba(0, 0, 0, 0.2), 0px 10px 14px 1px rgba(0, 0, 0, 0.14), 0px 4px 18px 3px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 6px 6px -3px rgba(0, 0, 0, 0.2), 0px 10px 14px 1px rgba(0, 0, 0, 0.14), 0px 4px 18px 3px rgba(0, 0, 0, 0.12); } .mat-elevation-z11 { - box-shadow: 0px 6px 7px -4px rgba(0, 0, 0, 0.2), 0px 11px 15px 1px rgba(0, 0, 0, 0.14), 0px 4px 20px 3px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 6px 7px -4px rgba(0, 0, 0, 0.2), 0px 11px 15px 1px rgba(0, 0, 0, 0.14), 0px 4px 20px 3px rgba(0, 0, 0, 0.12); } .mat-elevation-z12 { - box-shadow: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), 0px 5px 22px 4px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), 0px 5px 22px 4px rgba(0, 0, 0, 0.12); } .mat-elevation-z13 { - box-shadow: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 13px 19px 2px rgba(0, 0, 0, 0.14), 0px 5px 24px 4px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 13px 19px 2px rgba(0, 0, 0, 0.14), 0px 5px 24px 4px rgba(0, 0, 0, 0.12); } .mat-elevation-z14 { - box-shadow: 0px 7px 9px -4px rgba(0, 0, 0, 0.2), 0px 14px 21px 2px rgba(0, 0, 0, 0.14), 0px 5px 26px 4px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 7px 9px -4px rgba(0, 0, 0, 0.2), 0px 14px 21px 2px rgba(0, 0, 0, 0.14), 0px 5px 26px 4px rgba(0, 0, 0, 0.12); } .mat-elevation-z15 { - box-shadow: 0px 8px 9px -5px rgba(0, 0, 0, 0.2), 0px 15px 22px 2px rgba(0, 0, 0, 0.14), 0px 6px 28px 5px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 8px 9px -5px rgba(0, 0, 0, 0.2), 0px 15px 22px 2px rgba(0, 0, 0, 0.14), 0px 6px 28px 5px rgba(0, 0, 0, 0.12); } .mat-elevation-z16 { - box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), 0px 6px 30px 5px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), 0px 6px 30px 5px rgba(0, 0, 0, 0.12); } .mat-elevation-z17 { - box-shadow: 0px 8px 11px -5px rgba(0, 0, 0, 0.2), 0px 17px 26px 2px rgba(0, 0, 0, 0.14), 0px 6px 32px 5px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 8px 11px -5px rgba(0, 0, 0, 0.2), 0px 17px 26px 2px rgba(0, 0, 0, 0.14), 0px 6px 32px 5px rgba(0, 0, 0, 0.12); } .mat-elevation-z18 { - box-shadow: 0px 9px 11px -5px rgba(0, 0, 0, 0.2), 0px 18px 28px 2px rgba(0, 0, 0, 0.14), 0px 7px 34px 6px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 9px 11px -5px rgba(0, 0, 0, 0.2), 0px 18px 28px 2px rgba(0, 0, 0, 0.14), 0px 7px 34px 6px rgba(0, 0, 0, 0.12); } .mat-elevation-z19 { - box-shadow: 0px 9px 12px -6px rgba(0, 0, 0, 0.2), 0px 19px 29px 2px rgba(0, 0, 0, 0.14), 0px 7px 36px 6px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 9px 12px -6px rgba(0, 0, 0, 0.2), 0px 19px 29px 2px rgba(0, 0, 0, 0.14), 0px 7px 36px 6px rgba(0, 0, 0, 0.12); } .mat-elevation-z20 { - box-shadow: 0px 10px 13px -6px rgba(0, 0, 0, 0.2), 0px 20px 31px 3px rgba(0, 0, 0, 0.14), 0px 8px 38px 7px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 10px 13px -6px rgba(0, 0, 0, 0.2), 0px 20px 31px 3px rgba(0, 0, 0, 0.14), 0px 8px 38px 7px rgba(0, 0, 0, 0.12); } .mat-elevation-z21 { - box-shadow: 0px 10px 13px -6px rgba(0, 0, 0, 0.2), 0px 21px 33px 3px rgba(0, 0, 0, 0.14), 0px 8px 40px 7px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 10px 13px -6px rgba(0, 0, 0, 0.2), 0px 21px 33px 3px rgba(0, 0, 0, 0.14), 0px 8px 40px 7px rgba(0, 0, 0, 0.12); } .mat-elevation-z22 { - box-shadow: 0px 10px 14px -6px rgba(0, 0, 0, 0.2), 0px 22px 35px 3px rgba(0, 0, 0, 0.14), 0px 8px 42px 7px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 10px 14px -6px rgba(0, 0, 0, 0.2), 0px 22px 35px 3px rgba(0, 0, 0, 0.14), 0px 8px 42px 7px rgba(0, 0, 0, 0.12); } .mat-elevation-z23 { - box-shadow: 0px 11px 14px -7px rgba(0, 0, 0, 0.2), 0px 23px 36px 3px rgba(0, 0, 0, 0.14), 0px 9px 44px 8px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 11px 14px -7px rgba(0, 0, 0, 0.2), 0px 23px 36px 3px rgba(0, 0, 0, 0.14), 0px 9px 44px 8px rgba(0, 0, 0, 0.12); } .mat-elevation-z24 { - box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14), 0px 9px 46px 8px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14), 0px 9px 46px 8px rgba(0, 0, 0, 0.12); } .mat-theme-loaded-marker { - display: none; -} + display: none; } .mat-autocomplete-panel { - background: #393939; - color: white; -} - + background: #2b2b2b; + color: white; } .mat-autocomplete-panel:not([class*='mat-elevation-z']) { - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); -} - + box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); } .mat-autocomplete-panel .mat-option.mat-selected:not(.mat-active):not(:hover) { - background: #393939; -} - + background: #2b2b2b; } .mat-autocomplete-panel .mat-option.mat-selected:not(.mat-active):not(:hover):not(.mat-option-disabled) { - color: white; -} + color: white; } .mat-badge-content { - color: white; - background: #3358cc; -} - + color: rgba(18, 18, 18, 0.87); + background: #9fa8da; } .cdk-high-contrast-active .mat-badge-content { outline: solid 1px; - border-radius: 0; -} + border-radius: 0; } .mat-badge-accent .mat-badge-content { background: #797979; - color: white; -} + color: white; } .mat-badge-warn .mat-badge-content { color: white; - background: red; -} + background: red; } .mat-badge { - position: relative; -} + position: relative; } .mat-badge-hidden .mat-badge-content { - display: none; -} + display: none; } .mat-badge-disabled .mat-badge-content { - background: #6b6b6b; - color: rgba(255, 255, 255, 0.5); -} + background: #626262; + color: rgba(255, 255, 255, 0.5); } .mat-badge-content { position: absolute; @@ -963,1858 +748,1352 @@ body { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - pointer-events: none; -} + pointer-events: none; } .ng-animate-disabled .mat-badge-content, .mat-badge-content._mat-animation-noopable { - transition: none; -} + transition: none; } .mat-badge-content.mat-badge-active { - transform: none; -} + transform: none; } .mat-badge-small .mat-badge-content { width: 16px; height: 16px; - line-height: 16px; -} + line-height: 16px; } .mat-badge-small.mat-badge-above .mat-badge-content { - top: -8px; -} + top: -8px; } .mat-badge-small.mat-badge-below .mat-badge-content { - bottom: -8px; -} + bottom: -8px; } .mat-badge-small.mat-badge-before .mat-badge-content { - left: -16px; -} + left: -16px; } [dir='rtl'] .mat-badge-small.mat-badge-before .mat-badge-content { left: auto; - right: -16px; -} + right: -16px; } .mat-badge-small.mat-badge-after .mat-badge-content { - right: -16px; -} + right: -16px; } [dir='rtl'] .mat-badge-small.mat-badge-after .mat-badge-content { right: auto; - left: -16px; -} + left: -16px; } .mat-badge-small.mat-badge-overlap.mat-badge-before .mat-badge-content { - left: -8px; -} + left: -8px; } [dir='rtl'] .mat-badge-small.mat-badge-overlap.mat-badge-before .mat-badge-content { left: auto; - right: -8px; -} + right: -8px; } .mat-badge-small.mat-badge-overlap.mat-badge-after .mat-badge-content { - right: -8px; -} + right: -8px; } [dir='rtl'] .mat-badge-small.mat-badge-overlap.mat-badge-after .mat-badge-content { right: auto; - left: -8px; -} + left: -8px; } .mat-badge-medium .mat-badge-content { width: 22px; height: 22px; - line-height: 22px; -} + line-height: 22px; } .mat-badge-medium.mat-badge-above .mat-badge-content { - top: -11px; -} + top: -11px; } .mat-badge-medium.mat-badge-below .mat-badge-content { - bottom: -11px; -} + bottom: -11px; } .mat-badge-medium.mat-badge-before .mat-badge-content { - left: -22px; -} + left: -22px; } [dir='rtl'] .mat-badge-medium.mat-badge-before .mat-badge-content { left: auto; - right: -22px; -} + right: -22px; } .mat-badge-medium.mat-badge-after .mat-badge-content { - right: -22px; -} + right: -22px; } [dir='rtl'] .mat-badge-medium.mat-badge-after .mat-badge-content { right: auto; - left: -22px; -} + left: -22px; } .mat-badge-medium.mat-badge-overlap.mat-badge-before .mat-badge-content { - left: -11px; -} + left: -11px; } [dir='rtl'] .mat-badge-medium.mat-badge-overlap.mat-badge-before .mat-badge-content { left: auto; - right: -11px; -} + right: -11px; } .mat-badge-medium.mat-badge-overlap.mat-badge-after .mat-badge-content { - right: -11px; -} + right: -11px; } [dir='rtl'] .mat-badge-medium.mat-badge-overlap.mat-badge-after .mat-badge-content { right: auto; - left: -11px; -} + left: -11px; } .mat-badge-large .mat-badge-content { width: 28px; height: 28px; - line-height: 28px; -} + line-height: 28px; } .mat-badge-large.mat-badge-above .mat-badge-content { - top: -14px; -} + top: -14px; } .mat-badge-large.mat-badge-below .mat-badge-content { - bottom: -14px; -} + bottom: -14px; } .mat-badge-large.mat-badge-before .mat-badge-content { - left: -28px; -} + left: -28px; } [dir='rtl'] .mat-badge-large.mat-badge-before .mat-badge-content { left: auto; - right: -28px; -} + right: -28px; } .mat-badge-large.mat-badge-after .mat-badge-content { - right: -28px; -} + right: -28px; } [dir='rtl'] .mat-badge-large.mat-badge-after .mat-badge-content { right: auto; - left: -28px; -} + left: -28px; } .mat-badge-large.mat-badge-overlap.mat-badge-before .mat-badge-content { - left: -14px; -} + left: -14px; } [dir='rtl'] .mat-badge-large.mat-badge-overlap.mat-badge-before .mat-badge-content { left: auto; - right: -14px; -} + right: -14px; } .mat-badge-large.mat-badge-overlap.mat-badge-after .mat-badge-content { - right: -14px; -} + right: -14px; } [dir='rtl'] .mat-badge-large.mat-badge-overlap.mat-badge-after .mat-badge-content { right: auto; - left: -14px; -} + left: -14px; } .mat-bottom-sheet-container { box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), 0px 6px 30px 5px rgba(0, 0, 0, 0.12); - background: #393939; - color: white; -} + background: #2b2b2b; + color: white; } .mat-button, .mat-icon-button, .mat-stroked-button { color: inherit; - background: transparent; -} - + background: transparent; } .mat-button.mat-primary, .mat-icon-button.mat-primary, .mat-stroked-button.mat-primary { - color: #3358cc; -} - + color: #9fa8da; } .mat-button.mat-accent, .mat-icon-button.mat-accent, .mat-stroked-button.mat-accent { - color: #797979; -} - + color: #797979; } .mat-button.mat-warn, .mat-icon-button.mat-warn, .mat-stroked-button.mat-warn { - color: red; -} - + color: red; } .mat-button.mat-primary.mat-button-disabled, .mat-button.mat-accent.mat-button-disabled, .mat-button.mat-warn.mat-button-disabled, .mat-button.mat-button-disabled.mat-button-disabled, .mat-icon-button.mat-primary.mat-button-disabled, .mat-icon-button.mat-accent.mat-button-disabled, .mat-icon-button.mat-warn.mat-button-disabled, .mat-icon-button.mat-button-disabled.mat-button-disabled, .mat-stroked-button.mat-primary.mat-button-disabled, .mat-stroked-button.mat-accent.mat-button-disabled, .mat-stroked-button.mat-warn.mat-button-disabled, .mat-stroked-button.mat-button-disabled.mat-button-disabled { - color: rgba(255, 255, 255, 0.3); -} - + color: rgba(255, 255, 255, 0.3); } .mat-button.mat-primary .mat-button-focus-overlay, .mat-icon-button.mat-primary .mat-button-focus-overlay, .mat-stroked-button.mat-primary .mat-button-focus-overlay { - background-color: #3358cc; -} - + background-color: #9fa8da; } .mat-button.mat-accent .mat-button-focus-overlay, .mat-icon-button.mat-accent .mat-button-focus-overlay, .mat-stroked-button.mat-accent .mat-button-focus-overlay { - background-color: #797979; -} - + background-color: #797979; } .mat-button.mat-warn .mat-button-focus-overlay, .mat-icon-button.mat-warn .mat-button-focus-overlay, .mat-stroked-button.mat-warn .mat-button-focus-overlay { - background-color: red; -} - + background-color: red; } .mat-button.mat-button-disabled .mat-button-focus-overlay, .mat-icon-button.mat-button-disabled .mat-button-focus-overlay, .mat-stroked-button.mat-button-disabled .mat-button-focus-overlay { - background-color: transparent; -} - + background-color: transparent; } .mat-button .mat-ripple-element, .mat-icon-button .mat-ripple-element, .mat-stroked-button .mat-ripple-element { opacity: 0.1; - background-color: currentColor; -} + background-color: currentColor; } .mat-button-focus-overlay { - background: #ffffff; -} + background: #ffffff; } .mat-stroked-button:not(.mat-button-disabled) { - border-color: rgba(255, 255, 255, 0.12); -} + border-color: rgba(255, 255, 255, 0.12); } .mat-flat-button, .mat-raised-button, .mat-fab, .mat-mini-fab { color: white; - background-color: #393939; -} - + background-color: #2b2b2b; } .mat-flat-button.mat-primary, .mat-raised-button.mat-primary, .mat-fab.mat-primary, .mat-mini-fab.mat-primary { - color: white; -} - + color: rgba(18, 18, 18, 0.87); } .mat-flat-button.mat-accent, .mat-raised-button.mat-accent, .mat-fab.mat-accent, .mat-mini-fab.mat-accent { - color: white; -} - + color: white; } .mat-flat-button.mat-warn, .mat-raised-button.mat-warn, .mat-fab.mat-warn, .mat-mini-fab.mat-warn { - color: white; -} - + color: white; } .mat-flat-button.mat-primary.mat-button-disabled, .mat-flat-button.mat-accent.mat-button-disabled, .mat-flat-button.mat-warn.mat-button-disabled, .mat-flat-button.mat-button-disabled.mat-button-disabled, .mat-raised-button.mat-primary.mat-button-disabled, .mat-raised-button.mat-accent.mat-button-disabled, .mat-raised-button.mat-warn.mat-button-disabled, .mat-raised-button.mat-button-disabled.mat-button-disabled, .mat-fab.mat-primary.mat-button-disabled, .mat-fab.mat-accent.mat-button-disabled, .mat-fab.mat-warn.mat-button-disabled, .mat-fab.mat-button-disabled.mat-button-disabled, .mat-mini-fab.mat-primary.mat-button-disabled, .mat-mini-fab.mat-accent.mat-button-disabled, .mat-mini-fab.mat-warn.mat-button-disabled, .mat-mini-fab.mat-button-disabled.mat-button-disabled { - color: rgba(255, 255, 255, 0.3); -} - + color: rgba(255, 255, 255, 0.3); } .mat-flat-button.mat-primary, .mat-raised-button.mat-primary, .mat-fab.mat-primary, .mat-mini-fab.mat-primary { - background-color: #3358cc; -} - + background-color: #9fa8da; } .mat-flat-button.mat-accent, .mat-raised-button.mat-accent, .mat-fab.mat-accent, .mat-mini-fab.mat-accent { - background-color: #797979; -} - + background-color: #797979; } .mat-flat-button.mat-warn, .mat-raised-button.mat-warn, .mat-fab.mat-warn, .mat-mini-fab.mat-warn { - background-color: red; -} - + background-color: red; } .mat-flat-button.mat-primary.mat-button-disabled, .mat-flat-button.mat-accent.mat-button-disabled, .mat-flat-button.mat-warn.mat-button-disabled, .mat-flat-button.mat-button-disabled.mat-button-disabled, .mat-raised-button.mat-primary.mat-button-disabled, .mat-raised-button.mat-accent.mat-button-disabled, .mat-raised-button.mat-warn.mat-button-disabled, .mat-raised-button.mat-button-disabled.mat-button-disabled, .mat-fab.mat-primary.mat-button-disabled, .mat-fab.mat-accent.mat-button-disabled, .mat-fab.mat-warn.mat-button-disabled, .mat-fab.mat-button-disabled.mat-button-disabled, .mat-mini-fab.mat-primary.mat-button-disabled, .mat-mini-fab.mat-accent.mat-button-disabled, .mat-mini-fab.mat-warn.mat-button-disabled, .mat-mini-fab.mat-button-disabled.mat-button-disabled { - background-color: rgba(250, 250, 250, 0.12); -} - + background-color: rgba(250, 250, 250, 0.12); } .mat-flat-button.mat-primary .mat-ripple-element, .mat-raised-button.mat-primary .mat-ripple-element, .mat-fab.mat-primary .mat-ripple-element, .mat-mini-fab.mat-primary .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.1); -} - + background-color: rgba(18, 18, 18, 0.1); } .mat-flat-button.mat-accent .mat-ripple-element, .mat-raised-button.mat-accent .mat-ripple-element, .mat-fab.mat-accent .mat-ripple-element, .mat-mini-fab.mat-accent .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.1); -} - + background-color: rgba(255, 255, 255, 0.1); } .mat-flat-button.mat-warn .mat-ripple-element, .mat-raised-button.mat-warn .mat-ripple-element, .mat-fab.mat-warn .mat-ripple-element, .mat-mini-fab.mat-warn .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.1); -} + background-color: rgba(255, 255, 255, 0.1); } .mat-stroked-button:not([class*='mat-elevation-z']), .mat-flat-button:not([class*='mat-elevation-z']) { - box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); } .mat-raised-button:not([class*='mat-elevation-z']) { - box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); } .mat-raised-button:not(.mat-button-disabled):active:not([class*='mat-elevation-z']) { - box-shadow: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12); } .mat-raised-button.mat-button-disabled:not([class*='mat-elevation-z']) { - box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); } .mat-fab:not([class*='mat-elevation-z']), .mat-mini-fab:not([class*='mat-elevation-z']) { - box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); } .mat-fab:not(.mat-button-disabled):active:not([class*='mat-elevation-z']), .mat-mini-fab:not(.mat-button-disabled):active:not([class*='mat-elevation-z']) { - box-shadow: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), 0px 5px 22px 4px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 7px 8px -4px rgba(0, 0, 0, 0.2), 0px 12px 17px 2px rgba(0, 0, 0, 0.14), 0px 5px 22px 4px rgba(0, 0, 0, 0.12); } .mat-fab.mat-button-disabled:not([class*='mat-elevation-z']), .mat-mini-fab.mat-button-disabled:not([class*='mat-elevation-z']) { - box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); } .mat-button-toggle-standalone, .mat-button-toggle-group { - box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); } .mat-button-toggle-standalone.mat-button-toggle-appearance-standard, .mat-button-toggle-group-appearance-standard { - box-shadow: none; -} + box-shadow: none; } .mat-button-toggle { - color: rgba(255, 255, 255, 0.5); -} - + color: rgba(255, 255, 255, 0.5); } .mat-button-toggle .mat-button-toggle-focus-overlay { - background-color: rgba(255, 255, 255, 0.12); -} + background-color: rgba(255, 255, 255, 0.12); } .mat-button-toggle-appearance-standard { color: white; - background: #393939; -} - + background: #2b2b2b; } .mat-button-toggle-appearance-standard .mat-button-toggle-focus-overlay { - background-color: white; -} + background-color: white; } .mat-button-toggle-group-appearance-standard .mat-button-toggle + .mat-button-toggle { - border-left: solid 1px rgba(255, 255, 255, 0.12); -} + border-left: solid 1px rgba(255, 255, 255, 0.12); } [dir='rtl'] .mat-button-toggle-group-appearance-standard .mat-button-toggle + .mat-button-toggle { border-left: none; - border-right: solid 1px rgba(255, 255, 255, 0.12); -} + border-right: solid 1px rgba(255, 255, 255, 0.12); } .mat-button-toggle-group-appearance-standard.mat-button-toggle-vertical .mat-button-toggle + .mat-button-toggle { border-left: none; border-right: none; - border-top: solid 1px rgba(255, 255, 255, 0.12); -} + border-top: solid 1px rgba(255, 255, 255, 0.12); } .mat-button-toggle-checked { - background-color: #5f5f5f; - color: rgba(255, 255, 255, 0.7); -} - + background-color: #515151; + color: rgba(255, 255, 255, 0.7); } .mat-button-toggle-checked.mat-button-toggle-appearance-standard { - color: white; -} + color: white; } .mat-button-toggle-disabled { color: rgba(255, 255, 255, 0.3); - background-color: #464646; -} - + background-color: #383838; } .mat-button-toggle-disabled.mat-button-toggle-appearance-standard { - background: #393939; -} - + background: #2b2b2b; } .mat-button-toggle-disabled.mat-button-toggle-checked { - background-color: #797979; -} + background-color: #6b6b6b; } .mat-button-toggle-standalone.mat-button-toggle-appearance-standard, .mat-button-toggle-group-appearance-standard { - border: solid 1px rgba(255, 255, 255, 0.12); -} + border: solid 1px rgba(255, 255, 255, 0.12); } .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { - line-height: 48px; -} + line-height: 48px; } .mat-card { - background: #393939; - color: white; -} - + background: #2b2b2b; + color: white; } .mat-card:not([class*='mat-elevation-z']) { - box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); -} - + box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); } .mat-card.mat-card-flat:not([class*='mat-elevation-z']) { - box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); } .mat-card-subtitle { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-checkbox-frame { - border-color: rgba(255, 255, 255, 0.7); -} + border-color: rgba(255, 255, 255, 0.7); } .mat-checkbox-checkmark { - fill: #2c2c2c; -} + fill: #1e1e1e; } .mat-checkbox-checkmark-path { - stroke: #2c2c2c !important; -} + stroke: #1e1e1e !important; } .mat-checkbox-mixedmark { - background-color: #2c2c2c; -} + background-color: #1e1e1e; } .mat-checkbox-indeterminate.mat-primary .mat-checkbox-background, .mat-checkbox-checked.mat-primary .mat-checkbox-background { - background-color: #3358cc; -} + background-color: #9fa8da; } .mat-checkbox-indeterminate.mat-accent .mat-checkbox-background, .mat-checkbox-checked.mat-accent .mat-checkbox-background { - background-color: #797979; -} + background-color: #797979; } .mat-checkbox-indeterminate.mat-warn .mat-checkbox-background, .mat-checkbox-checked.mat-warn .mat-checkbox-background { - background-color: red; -} + background-color: red; } .mat-checkbox-disabled.mat-checkbox-checked .mat-checkbox-background, .mat-checkbox-disabled.mat-checkbox-indeterminate .mat-checkbox-background { - background-color: #686868; -} + background-color: #686868; } .mat-checkbox-disabled:not(.mat-checkbox-checked) .mat-checkbox-frame { - border-color: #686868; -} + border-color: #686868; } .mat-checkbox-disabled .mat-checkbox-label { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-checkbox .mat-ripple-element { - background-color: #ffffff; -} + background-color: #ffffff; } .mat-checkbox-checked:not(.mat-checkbox-disabled).mat-primary .mat-ripple-element, .mat-checkbox:active:not(.mat-checkbox-disabled).mat-primary .mat-ripple-element { - background: #3358cc; -} + background: #9fa8da; } .mat-checkbox-checked:not(.mat-checkbox-disabled).mat-accent .mat-ripple-element, .mat-checkbox:active:not(.mat-checkbox-disabled).mat-accent .mat-ripple-element { - background: #797979; -} + background: #797979; } .mat-checkbox-checked:not(.mat-checkbox-disabled).mat-warn .mat-ripple-element, .mat-checkbox:active:not(.mat-checkbox-disabled).mat-warn .mat-ripple-element { - background: red; -} + background: red; } .mat-chip.mat-standard-chip { - background-color: #5f5f5f; - color: white; -} - + background-color: #515151; + color: white; } .mat-chip.mat-standard-chip .mat-chip-remove { color: white; - opacity: 0.4; -} - + opacity: 0.4; } .mat-chip.mat-standard-chip:not(.mat-chip-disabled):active { - box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 0px 1px 8px 0px rgba(0, 0, 0, 0.12); -} - + box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 0px 1px 8px 0px rgba(0, 0, 0, 0.12); } .mat-chip.mat-standard-chip:not(.mat-chip-disabled) .mat-chip-remove:hover { - opacity: 0.54; -} - + opacity: 0.54; } .mat-chip.mat-standard-chip.mat-chip-disabled { - opacity: 0.4; -} - + opacity: 0.4; } .mat-chip.mat-standard-chip::after { - background: #ffffff; -} + background: #ffffff; } .mat-chip.mat-standard-chip.mat-chip-selected.mat-primary { - background-color: #3358cc; - color: white; -} - + background-color: #9fa8da; + color: rgba(18, 18, 18, 0.87); } .mat-chip.mat-standard-chip.mat-chip-selected.mat-primary .mat-chip-remove { - color: white; - opacity: 0.4; -} - + color: rgba(18, 18, 18, 0.87); + opacity: 0.4; } .mat-chip.mat-standard-chip.mat-chip-selected.mat-primary .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.1); -} + background-color: rgba(18, 18, 18, 0.1); } .mat-chip.mat-standard-chip.mat-chip-selected.mat-warn { background-color: red; - color: white; -} - + color: white; } .mat-chip.mat-standard-chip.mat-chip-selected.mat-warn .mat-chip-remove { color: white; - opacity: 0.4; -} - + opacity: 0.4; } .mat-chip.mat-standard-chip.mat-chip-selected.mat-warn .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.1); -} + background-color: rgba(255, 255, 255, 0.1); } .mat-chip.mat-standard-chip.mat-chip-selected.mat-accent { background-color: #797979; - color: white; -} - + color: white; } .mat-chip.mat-standard-chip.mat-chip-selected.mat-accent .mat-chip-remove { color: white; - opacity: 0.4; -} - + opacity: 0.4; } .mat-chip.mat-standard-chip.mat-chip-selected.mat-accent .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.1); -} + background-color: rgba(255, 255, 255, 0.1); } .mat-table { - background: #393939; -} + background: #2b2b2b; } .mat-table thead, .mat-table tbody, .mat-table tfoot, mat-header-row, mat-row, mat-footer-row, [mat-header-row], [mat-row], [mat-footer-row], .mat-table-sticky { - background: inherit; -} + background: inherit; } mat-row, mat-header-row, mat-footer-row, th.mat-header-cell, td.mat-cell, td.mat-footer-cell { - border-bottom-color: rgba(255, 255, 255, 0.12); -} + border-bottom-color: rgba(255, 255, 255, 0.12); } .mat-header-cell { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-cell, .mat-footer-cell { - color: white; -} + color: white; } .mat-calendar-arrow { - border-top-color: white; -} + border-top-color: white; } .mat-datepicker-toggle, .mat-datepicker-content .mat-calendar-next-button, .mat-datepicker-content .mat-calendar-previous-button { - color: white; -} + color: white; } .mat-calendar-table-header { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-calendar-table-header-divider::after { - background: rgba(255, 255, 255, 0.12); -} + background: rgba(255, 255, 255, 0.12); } .mat-calendar-body-label { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-calendar-body-cell-content, .mat-date-range-input-separator { color: white; - border-color: transparent; -} + border-color: transparent; } .mat-calendar-body-disabled > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical) { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-form-field-disabled .mat-date-range-input-separator { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-calendar-body-in-preview { - color: rgba(255, 255, 255, 0.24); -} + color: rgba(255, 255, 255, 0.24); } .mat-calendar-body-today:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical) { - border-color: rgba(255, 255, 255, 0.5); -} + border-color: rgba(255, 255, 255, 0.5); } .mat-calendar-body-disabled > .mat-calendar-body-today:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical) { - border-color: rgba(255, 255, 255, 0.3); -} + border-color: rgba(255, 255, 255, 0.3); } .mat-calendar-body-in-range::before { - background: rgba(51, 88, 204, 0.2); -} + background: rgba(159, 168, 218, 0.2); } .mat-calendar-body-comparison-identical, .mat-calendar-body-in-comparison-range::before { - background: rgba(249, 171, 0, 0.2); -} + background: rgba(249, 171, 0, 0.2); } .mat-calendar-body-comparison-bridge-start::before, [dir='rtl'] .mat-calendar-body-comparison-bridge-end::before { - background: linear-gradient(to right, rgba(51, 88, 204, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); -} + background: linear-gradient(to right, rgba(159, 168, 218, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); } .mat-calendar-body-comparison-bridge-end::before, [dir='rtl'] .mat-calendar-body-comparison-bridge-start::before { - background: linear-gradient(to left, rgba(51, 88, 204, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); -} + background: linear-gradient(to left, rgba(159, 168, 218, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); } .mat-calendar-body-in-range > .mat-calendar-body-comparison-identical, .mat-calendar-body-in-comparison-range.mat-calendar-body-in-range::after { - background: #a8dab5; -} + background: #a8dab5; } .mat-calendar-body-comparison-identical.mat-calendar-body-selected, .mat-calendar-body-in-comparison-range > .mat-calendar-body-selected { - background: #46a35e; -} + background: #46a35e; } .mat-calendar-body-selected { - background-color: #3358cc; - color: white; -} + background-color: #9fa8da; + color: rgba(18, 18, 18, 0.87); } .mat-calendar-body-disabled > .mat-calendar-body-selected { - background-color: rgba(51, 88, 204, 0.4); -} + background-color: rgba(159, 168, 218, 0.4); } .mat-calendar-body-today.mat-calendar-body-selected { - box-shadow: inset 0 0 0 1px white; -} + box-shadow: inset 0 0 0 1px rgba(18, 18, 18, 0.87); } .mat-calendar-body-cell:not(.mat-calendar-body-disabled):hover > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical), .cdk-keyboard-focused .mat-calendar-body-active > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical), .cdk-program-focused .mat-calendar-body-active > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical) { - background-color: rgba(51, 88, 204, 0.3); -} + background-color: rgba(159, 168, 218, 0.3); } .mat-datepicker-content { box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); - background-color: #393939; - color: white; -} - + background-color: #2b2b2b; + color: white; } .mat-datepicker-content.mat-accent .mat-calendar-body-in-range::before { - background: rgba(121, 121, 121, 0.2); -} - + background: rgba(121, 121, 121, 0.2); } .mat-datepicker-content.mat-accent .mat-calendar-body-comparison-identical, .mat-datepicker-content.mat-accent .mat-calendar-body-in-comparison-range::before { - background: rgba(249, 171, 0, 0.2); -} - + background: rgba(249, 171, 0, 0.2); } .mat-datepicker-content.mat-accent .mat-calendar-body-comparison-bridge-start::before, .mat-datepicker-content.mat-accent [dir='rtl'] .mat-calendar-body-comparison-bridge-end::before { - background: linear-gradient(to right, rgba(121, 121, 121, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); -} - + background: linear-gradient(to right, rgba(121, 121, 121, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); } .mat-datepicker-content.mat-accent .mat-calendar-body-comparison-bridge-end::before, .mat-datepicker-content.mat-accent [dir='rtl'] .mat-calendar-body-comparison-bridge-start::before { - background: linear-gradient(to left, rgba(121, 121, 121, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); -} - + background: linear-gradient(to left, rgba(121, 121, 121, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); } .mat-datepicker-content.mat-accent .mat-calendar-body-in-range > .mat-calendar-body-comparison-identical, .mat-datepicker-content.mat-accent .mat-calendar-body-in-comparison-range.mat-calendar-body-in-range::after { - background: #a8dab5; -} - + background: #a8dab5; } .mat-datepicker-content.mat-accent .mat-calendar-body-comparison-identical.mat-calendar-body-selected, .mat-datepicker-content.mat-accent .mat-calendar-body-in-comparison-range > .mat-calendar-body-selected { - background: #46a35e; -} - + background: #46a35e; } .mat-datepicker-content.mat-accent .mat-calendar-body-selected { background-color: #797979; - color: white; -} - + color: white; } .mat-datepicker-content.mat-accent .mat-calendar-body-disabled > .mat-calendar-body-selected { - background-color: rgba(121, 121, 121, 0.4); -} - + background-color: rgba(121, 121, 121, 0.4); } .mat-datepicker-content.mat-accent .mat-calendar-body-today.mat-calendar-body-selected { - box-shadow: inset 0 0 0 1px white; -} - + box-shadow: inset 0 0 0 1px white; } .mat-datepicker-content.mat-accent .mat-calendar-body-cell:not(.mat-calendar-body-disabled):hover > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical), .mat-datepicker-content.mat-accent .cdk-keyboard-focused .mat-calendar-body-active > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical), .mat-datepicker-content.mat-accent .cdk-program-focused .mat-calendar-body-active > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical) { - background-color: rgba(121, 121, 121, 0.3); -} - + background-color: rgba(121, 121, 121, 0.3); } .mat-datepicker-content.mat-warn .mat-calendar-body-in-range::before { - background: rgba(255, 0, 0, 0.2); -} - + background: rgba(255, 0, 0, 0.2); } .mat-datepicker-content.mat-warn .mat-calendar-body-comparison-identical, .mat-datepicker-content.mat-warn .mat-calendar-body-in-comparison-range::before { - background: rgba(249, 171, 0, 0.2); -} - + background: rgba(249, 171, 0, 0.2); } .mat-datepicker-content.mat-warn .mat-calendar-body-comparison-bridge-start::before, .mat-datepicker-content.mat-warn [dir='rtl'] .mat-calendar-body-comparison-bridge-end::before { - background: linear-gradient(to right, rgba(255, 0, 0, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); -} - + background: linear-gradient(to right, rgba(255, 0, 0, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); } .mat-datepicker-content.mat-warn .mat-calendar-body-comparison-bridge-end::before, .mat-datepicker-content.mat-warn [dir='rtl'] .mat-calendar-body-comparison-bridge-start::before { - background: linear-gradient(to left, rgba(255, 0, 0, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); -} - + background: linear-gradient(to left, rgba(255, 0, 0, 0.2) 50%, rgba(249, 171, 0, 0.2) 50%); } .mat-datepicker-content.mat-warn .mat-calendar-body-in-range > .mat-calendar-body-comparison-identical, .mat-datepicker-content.mat-warn .mat-calendar-body-in-comparison-range.mat-calendar-body-in-range::after { - background: #a8dab5; -} - + background: #a8dab5; } .mat-datepicker-content.mat-warn .mat-calendar-body-comparison-identical.mat-calendar-body-selected, .mat-datepicker-content.mat-warn .mat-calendar-body-in-comparison-range > .mat-calendar-body-selected { - background: #46a35e; -} - + background: #46a35e; } .mat-datepicker-content.mat-warn .mat-calendar-body-selected { background-color: red; - color: white; -} - + color: white; } .mat-datepicker-content.mat-warn .mat-calendar-body-disabled > .mat-calendar-body-selected { - background-color: rgba(255, 0, 0, 0.4); -} - + background-color: rgba(255, 0, 0, 0.4); } .mat-datepicker-content.mat-warn .mat-calendar-body-today.mat-calendar-body-selected { - box-shadow: inset 0 0 0 1px white; -} - + box-shadow: inset 0 0 0 1px white; } .mat-datepicker-content.mat-warn .mat-calendar-body-cell:not(.mat-calendar-body-disabled):hover > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical), .mat-datepicker-content.mat-warn .cdk-keyboard-focused .mat-calendar-body-active > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical), .mat-datepicker-content.mat-warn .cdk-program-focused .mat-calendar-body-active > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-comparison-identical) { - background-color: rgba(255, 0, 0, 0.3); -} + background-color: rgba(255, 0, 0, 0.3); } .mat-datepicker-content-touch { - box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12); } .mat-datepicker-toggle-active { - color: #3358cc; -} - + color: #9fa8da; } .mat-datepicker-toggle-active.mat-accent { - color: #797979; -} - + color: #797979; } .mat-datepicker-toggle-active.mat-warn { - color: red; -} + color: red; } .mat-date-range-input-inner[disabled] { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-dialog-container { box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14), 0px 9px 46px 8px rgba(0, 0, 0, 0.12); - background: #393939; - color: white; -} + background: #2b2b2b; + color: white; } .mat-divider { - border-top-color: rgba(255, 255, 255, 0.12); -} + border-top-color: rgba(255, 255, 255, 0.12); } .mat-divider-vertical { - border-right-color: rgba(255, 255, 255, 0.12); -} + border-right-color: rgba(255, 255, 255, 0.12); } .mat-expansion-panel { - background: #393939; - color: white; -} - + background: #2b2b2b; + color: white; } .mat-expansion-panel:not([class*='mat-elevation-z']) { - box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); } .mat-action-row { - border-top-color: rgba(255, 255, 255, 0.12); -} + border-top-color: rgba(255, 255, 255, 0.12); } .mat-expansion-panel .mat-expansion-panel-header.cdk-keyboard-focused:not([aria-disabled='true']), .mat-expansion-panel .mat-expansion-panel-header.cdk-program-focused:not([aria-disabled='true']), .mat-expansion-panel:not(.mat-expanded) .mat-expansion-panel-header:hover:not([aria-disabled='true']) { - background: rgba(250, 250, 250, 0.04); -} + background: rgba(250, 250, 250, 0.04); } @media (hover: none) { .mat-expansion-panel:not(.mat-expanded):not([aria-disabled='true']) .mat-expansion-panel-header:hover { - background: #393939; - } -} + background: #2b2b2b; } } .mat-expansion-panel-header-title { - color: white; -} + color: white; } .mat-expansion-panel-header-description, .mat-expansion-indicator::after { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-expansion-panel-header[aria-disabled='true'] { - color: rgba(255, 255, 255, 0.3); -} - + color: rgba(255, 255, 255, 0.3); } .mat-expansion-panel-header[aria-disabled='true'] .mat-expansion-panel-header-title, .mat-expansion-panel-header[aria-disabled='true'] .mat-expansion-panel-header-description { - color: inherit; -} + color: inherit; } .mat-expansion-panel-header { - height: 48px; -} - + height: 48px; } .mat-expansion-panel-header.mat-expanded { - height: 64px; -} + height: 64px; } .mat-form-field-label { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-hint { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-form-field.mat-focused .mat-form-field-label { - color: #3358cc; -} - + color: #9fa8da; } .mat-form-field.mat-focused .mat-form-field-label.mat-accent { - color: #797979; -} - + color: #797979; } .mat-form-field.mat-focused .mat-form-field-label.mat-warn { - color: red; -} + color: red; } .mat-focused .mat-form-field-required-marker { - color: #797979; -} + color: #797979; } .mat-form-field-ripple { - background-color: white; -} + background-color: white; } .mat-form-field.mat-focused .mat-form-field-ripple { - background-color: #3358cc; -} - + background-color: #9fa8da; } .mat-form-field.mat-focused .mat-form-field-ripple.mat-accent { - background-color: #797979; -} - + background-color: #797979; } .mat-form-field.mat-focused .mat-form-field-ripple.mat-warn { - background-color: red; -} + background-color: red; } .mat-form-field-type-mat-native-select.mat-focused:not(.mat-form-field-invalid) .mat-form-field-infix::after { - color: #3358cc; -} + color: #9fa8da; } .mat-form-field-type-mat-native-select.mat-focused:not(.mat-form-field-invalid).mat-accent .mat-form-field-infix::after { - color: #797979; -} + color: #797979; } .mat-form-field-type-mat-native-select.mat-focused:not(.mat-form-field-invalid).mat-warn .mat-form-field-infix::after { - color: red; -} + color: red; } .mat-form-field.mat-form-field-invalid .mat-form-field-label { - color: red; -} - + color: red; } .mat-form-field.mat-form-field-invalid .mat-form-field-label.mat-accent, .mat-form-field.mat-form-field-invalid .mat-form-field-label .mat-form-field-required-marker { - color: red; -} + color: red; } .mat-form-field.mat-form-field-invalid .mat-form-field-ripple, .mat-form-field.mat-form-field-invalid .mat-form-field-ripple.mat-accent { - background-color: red; -} + background-color: red; } .mat-error { - color: red; -} + color: red; } .mat-form-field-appearance-legacy .mat-form-field-label { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-form-field-appearance-legacy .mat-hint { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-form-field-appearance-legacy .mat-form-field-underline { - background-color: rgba(255, 255, 255, 0.7); -} + background-color: rgba(255, 255, 255, 0.7); } .mat-form-field-appearance-legacy.mat-form-field-disabled .mat-form-field-underline { background-image: linear-gradient(to right, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0.7) 33%, transparent 0%); background-size: 4px 100%; - background-repeat: repeat-x; -} + background-repeat: repeat-x; } .mat-form-field-appearance-standard .mat-form-field-underline { - background-color: rgba(255, 255, 255, 0.7); -} + background-color: rgba(255, 255, 255, 0.7); } .mat-form-field-appearance-standard.mat-form-field-disabled .mat-form-field-underline { background-image: linear-gradient(to right, rgba(255, 255, 255, 0.7) 0%, rgba(255, 255, 255, 0.7) 33%, transparent 0%); background-size: 4px 100%; - background-repeat: repeat-x; -} + background-repeat: repeat-x; } .mat-form-field-appearance-fill .mat-form-field-flex { - background-color: rgba(255, 255, 255, 0.1); -} + background-color: rgba(255, 255, 255, 0.1); } .mat-form-field-appearance-fill.mat-form-field-disabled .mat-form-field-flex { - background-color: rgba(255, 255, 255, 0.05); -} + background-color: rgba(255, 255, 255, 0.05); } .mat-form-field-appearance-fill .mat-form-field-underline::before { - background-color: rgba(255, 255, 255, 0.5); -} + background-color: rgba(255, 255, 255, 0.5); } .mat-form-field-appearance-fill.mat-form-field-disabled .mat-form-field-label { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-form-field-appearance-fill.mat-form-field-disabled .mat-form-field-underline::before { - background-color: transparent; -} + background-color: transparent; } .mat-form-field-appearance-outline .mat-form-field-outline { - color: rgba(255, 255, 255, 0.3); -} + color: rgba(255, 255, 255, 0.3); } .mat-form-field-appearance-outline .mat-form-field-outline-thick { - color: white; -} + color: white; } .mat-form-field-appearance-outline.mat-focused .mat-form-field-outline-thick { - color: #3358cc; -} + color: #9fa8da; } .mat-form-field-appearance-outline.mat-focused.mat-accent .mat-form-field-outline-thick { - color: #797979; -} + color: #797979; } .mat-form-field-appearance-outline.mat-focused.mat-warn .mat-form-field-outline-thick { - color: red; -} + color: red; } .mat-form-field-appearance-outline.mat-form-field-invalid.mat-form-field-invalid .mat-form-field-outline-thick { - color: red; -} + color: red; } .mat-form-field-appearance-outline.mat-form-field-disabled .mat-form-field-label { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-form-field-appearance-outline.mat-form-field-disabled .mat-form-field-outline { - color: rgba(255, 255, 255, 0.15); -} + color: rgba(255, 255, 255, 0.15); } .mat-icon.mat-primary { - color: #3358cc; -} + color: #9fa8da; } .mat-icon.mat-accent { - color: #797979; -} + color: #797979; } .mat-icon.mat-warn { - color: red; -} + color: red; } .mat-form-field-type-mat-native-select .mat-form-field-infix::after { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-input-element:disabled, .mat-form-field-type-mat-native-select.mat-form-field-disabled .mat-form-field-infix::after { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-input-element { - caret-color: #3358cc; -} - + caret-color: #9fa8da; } .mat-input-element::placeholder { - color: rgba(255, 255, 255, 0.5); -} - + color: rgba(255, 255, 255, 0.5); } .mat-input-element::-moz-placeholder { - color: rgba(255, 255, 255, 0.5); -} - + color: rgba(255, 255, 255, 0.5); } .mat-input-element::-webkit-input-placeholder { - color: rgba(255, 255, 255, 0.5); -} - + color: rgba(255, 255, 255, 0.5); } .mat-input-element:-ms-input-placeholder { - color: rgba(255, 255, 255, 0.5); -} - + color: rgba(255, 255, 255, 0.5); } .mat-input-element option { - color: rgba(0, 0, 0, 0.87); -} - + color: rgba(18, 18, 18, 0.87); } .mat-input-element option:disabled { - color: rgba(0, 0, 0, 0.38); -} + color: rgba(18, 18, 18, 0.38); } .mat-form-field.mat-accent .mat-input-element { - caret-color: #797979; -} + caret-color: #797979; } .mat-form-field.mat-warn .mat-input-element, .mat-form-field-invalid .mat-input-element { - caret-color: red; -} + caret-color: red; } .mat-form-field-type-mat-native-select.mat-form-field-invalid .mat-form-field-infix::after { - color: red; -} + color: red; } .mat-list-base .mat-list-item { - color: white; -} + color: white; } .mat-list-base .mat-list-option { - color: white; -} + color: white; } .mat-list-base .mat-subheader { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-list-item-disabled { - background-color: #464646; -} + background-color: #383838; } .mat-list-option:hover, .mat-list-option:focus, .mat-nav-list .mat-list-item:hover, .mat-nav-list .mat-list-item:focus, .mat-action-list .mat-list-item:hover, .mat-action-list .mat-list-item:focus { - background: rgba(250, 250, 250, 0.04); -} + background: rgba(250, 250, 250, 0.04); } .mat-list-single-selected-option, .mat-list-single-selected-option:hover, .mat-list-single-selected-option:focus { - background: rgba(250, 250, 250, 0.12); -} + background: rgba(250, 250, 250, 0.12); } .mat-menu-panel { - background: #393939; -} - + background: #2b2b2b; } .mat-menu-panel:not([class*='mat-elevation-z']) { - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); } .mat-menu-item { background: transparent; - color: white; -} - + color: white; } .mat-menu-item[disabled], .mat-menu-item[disabled]::after, .mat-menu-item[disabled] .mat-icon-no-color { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-menu-item .mat-icon-no-color, .mat-menu-item-submenu-trigger::after { - color: white; -} + color: white; } .mat-menu-item:hover:not([disabled]), .mat-menu-item.cdk-program-focused:not([disabled]), .mat-menu-item.cdk-keyboard-focused:not([disabled]), .mat-menu-item-highlighted:not([disabled]) { - background: rgba(250, 250, 250, 0.04); -} + background: rgba(250, 250, 250, 0.04); } .mat-paginator { - background: #393939; -} + background: #2b2b2b; } .mat-paginator, .mat-paginator-page-size .mat-select-trigger { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-paginator-decrement, .mat-paginator-increment { border-top: 2px solid white; - border-right: 2px solid white; -} + border-right: 2px solid white; } .mat-paginator-first, .mat-paginator-last { - border-top: 2px solid white; -} + border-top: 2px solid white; } .mat-icon-button[disabled] .mat-paginator-decrement, .mat-icon-button[disabled] .mat-paginator-increment, .mat-icon-button[disabled] .mat-paginator-first, .mat-icon-button[disabled] .mat-paginator-last { - border-color: rgba(255, 255, 255, 0.5); -} + border-color: rgba(255, 255, 255, 0.5); } .mat-paginator-container { - min-height: 56px; -} + min-height: 56px; } .mat-progress-bar-background { - fill: #c2cdf0; -} + fill: #e2e5f4; } .mat-progress-bar-buffer { - background-color: #c2cdf0; -} + background-color: #e2e5f4; } .mat-progress-bar-fill::after { - background-color: #3358cc; -} + background-color: #9fa8da; } .mat-progress-bar.mat-accent .mat-progress-bar-background { - fill: #d7d7d7; -} + fill: #d7d7d7; } .mat-progress-bar.mat-accent .mat-progress-bar-buffer { - background-color: #d7d7d7; -} + background-color: #d7d7d7; } .mat-progress-bar.mat-accent .mat-progress-bar-fill::after { - background-color: #797979; -} + background-color: #797979; } .mat-progress-bar.mat-warn .mat-progress-bar-background { - fill: #ffb3b3; -} + fill: #ffb3b3; } .mat-progress-bar.mat-warn .mat-progress-bar-buffer { - background-color: #ffb3b3; -} + background-color: #ffb3b3; } .mat-progress-bar.mat-warn .mat-progress-bar-fill::after { - background-color: red; -} + background-color: red; } .mat-progress-spinner circle, .mat-spinner circle { - stroke: #3358cc; -} + stroke: #9fa8da; } .mat-progress-spinner.mat-accent circle, .mat-spinner.mat-accent circle { - stroke: #797979; -} + stroke: #797979; } .mat-progress-spinner.mat-warn circle, .mat-spinner.mat-warn circle { - stroke: red; -} + stroke: red; } .mat-radio-outer-circle { - border-color: rgba(255, 255, 255, 0.7); -} + border-color: rgba(255, 255, 255, 0.7); } .mat-radio-button.mat-primary.mat-radio-checked .mat-radio-outer-circle { - border-color: #3358cc; -} + border-color: #9fa8da; } .mat-radio-button.mat-primary .mat-radio-inner-circle, .mat-radio-button.mat-primary .mat-radio-ripple .mat-ripple-element:not(.mat-radio-persistent-ripple), .mat-radio-button.mat-primary.mat-radio-checked .mat-radio-persistent-ripple, .mat-radio-button.mat-primary:active .mat-radio-persistent-ripple { - background-color: #3358cc; -} + background-color: #9fa8da; } .mat-radio-button.mat-accent.mat-radio-checked .mat-radio-outer-circle { - border-color: #797979; -} + border-color: #797979; } .mat-radio-button.mat-accent .mat-radio-inner-circle, .mat-radio-button.mat-accent .mat-radio-ripple .mat-ripple-element:not(.mat-radio-persistent-ripple), .mat-radio-button.mat-accent.mat-radio-checked .mat-radio-persistent-ripple, .mat-radio-button.mat-accent:active .mat-radio-persistent-ripple { - background-color: #797979; -} + background-color: #797979; } .mat-radio-button.mat-warn.mat-radio-checked .mat-radio-outer-circle { - border-color: red; -} + border-color: red; } .mat-radio-button.mat-warn .mat-radio-inner-circle, .mat-radio-button.mat-warn .mat-radio-ripple .mat-ripple-element:not(.mat-radio-persistent-ripple), .mat-radio-button.mat-warn.mat-radio-checked .mat-radio-persistent-ripple, .mat-radio-button.mat-warn:active .mat-radio-persistent-ripple { - background-color: red; -} + background-color: red; } .mat-radio-button.mat-radio-disabled.mat-radio-checked .mat-radio-outer-circle, .mat-radio-button.mat-radio-disabled .mat-radio-outer-circle { - border-color: rgba(255, 255, 255, 0.5); -} + border-color: rgba(255, 255, 255, 0.5); } .mat-radio-button.mat-radio-disabled .mat-radio-ripple .mat-ripple-element, .mat-radio-button.mat-radio-disabled .mat-radio-inner-circle { - background-color: rgba(255, 255, 255, 0.5); -} + background-color: rgba(255, 255, 255, 0.5); } .mat-radio-button.mat-radio-disabled .mat-radio-label-content { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-radio-button .mat-ripple-element { - background-color: #ffffff; -} + background-color: #ffffff; } .mat-select-value { - color: white; -} + color: white; } .mat-select-placeholder { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-select-disabled .mat-select-value { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-select-arrow { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-select-panel { - background: #393939; -} - + background: #2b2b2b; } .mat-select-panel:not([class*='mat-elevation-z']) { - box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); -} - + box-shadow: 0px 2px 4px -1px rgba(0, 0, 0, 0.2), 0px 4px 5px 0px rgba(0, 0, 0, 0.14), 0px 1px 10px 0px rgba(0, 0, 0, 0.12); } .mat-select-panel .mat-option.mat-selected:not(.mat-option-multiple) { - background: rgba(250, 250, 250, 0.12); -} + background: rgba(250, 250, 250, 0.12); } .mat-form-field.mat-focused.mat-primary .mat-select-arrow { - color: #3358cc; -} + color: #9fa8da; } .mat-form-field.mat-focused.mat-accent .mat-select-arrow { - color: #797979; -} + color: #797979; } .mat-form-field.mat-focused.mat-warn .mat-select-arrow { - color: red; -} + color: red; } .mat-form-field .mat-select.mat-select-invalid .mat-select-arrow { - color: red; -} + color: red; } .mat-form-field .mat-select.mat-select-disabled .mat-select-arrow { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-drawer-container { - background-color: #2c2c2c; - color: white; -} + background-color: #1e1e1e; + color: white; } .mat-drawer { - background-color: #393939; - color: white; -} - + background-color: #2b2b2b; + color: white; } .mat-drawer.mat-drawer-push { - background-color: #393939; -} - + background-color: #2b2b2b; } .mat-drawer:not(.mat-drawer-side) { - box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), 0px 6px 30px 5px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 8px 10px -5px rgba(0, 0, 0, 0.2), 0px 16px 24px 2px rgba(0, 0, 0, 0.14), 0px 6px 30px 5px rgba(0, 0, 0, 0.12); } .mat-drawer-side { - border-right: solid 1px rgba(255, 255, 255, 0.12); -} - + border-right: solid 1px rgba(255, 255, 255, 0.12); } .mat-drawer-side.mat-drawer-end { border-left: solid 1px rgba(255, 255, 255, 0.12); - border-right: none; -} + border-right: none; } [dir='rtl'] .mat-drawer-side { border-left: solid 1px rgba(255, 255, 255, 0.12); - border-right: none; -} - + border-right: none; } [dir='rtl'] .mat-drawer-side.mat-drawer-end { border-left: none; - border-right: solid 1px rgba(255, 255, 255, 0.12); -} + border-right: solid 1px rgba(255, 255, 255, 0.12); } .mat-drawer-backdrop.mat-drawer-shown { - background-color: rgba(198, 198, 198, 0.6); -} + background-color: rgba(212, 212, 212, 0.6); } .mat-slide-toggle.mat-checked .mat-slide-toggle-thumb { - background-color: #797979; -} + background-color: #797979; } .mat-slide-toggle.mat-checked .mat-slide-toggle-bar { - background-color: rgba(121, 121, 121, 0.54); -} + background-color: rgba(121, 121, 121, 0.54); } .mat-slide-toggle.mat-checked .mat-ripple-element { - background-color: #797979; -} + background-color: #797979; } .mat-slide-toggle.mat-primary.mat-checked .mat-slide-toggle-thumb { - background-color: #3358cc; -} + background-color: #9fa8da; } .mat-slide-toggle.mat-primary.mat-checked .mat-slide-toggle-bar { - background-color: rgba(51, 88, 204, 0.54); -} + background-color: rgba(159, 168, 218, 0.54); } .mat-slide-toggle.mat-primary.mat-checked .mat-ripple-element { - background-color: #3358cc; -} + background-color: #9fa8da; } .mat-slide-toggle.mat-warn.mat-checked .mat-slide-toggle-thumb { - background-color: red; -} + background-color: red; } .mat-slide-toggle.mat-warn.mat-checked .mat-slide-toggle-bar { - background-color: rgba(255, 0, 0, 0.54); -} + background-color: rgba(255, 0, 0, 0.54); } .mat-slide-toggle.mat-warn.mat-checked .mat-ripple-element { - background-color: red; -} + background-color: red; } .mat-slide-toggle:not(.mat-checked) .mat-ripple-element { - background-color: white; -} + background-color: white; } .mat-slide-toggle-thumb { box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); - background-color: #bdbdbd; -} + background-color: #bdbdbd; } .mat-slide-toggle-bar { - background-color: rgba(255, 255, 255, 0.5); -} + background-color: rgba(255, 255, 255, 0.5); } .mat-slider-track-background { - background-color: rgba(255, 255, 255, 0.3); -} + background-color: rgba(255, 255, 255, 0.3); } .mat-primary .mat-slider-track-fill, .mat-primary .mat-slider-thumb, .mat-primary .mat-slider-thumb-label { - background-color: #3358cc; -} + background-color: #9fa8da; } .mat-primary .mat-slider-thumb-label-text { - color: white; -} + color: rgba(18, 18, 18, 0.87); } .mat-primary .mat-slider-focus-ring { - background-color: rgba(51, 88, 204, 0.2); -} + background-color: rgba(159, 168, 218, 0.2); } .mat-accent .mat-slider-track-fill, .mat-accent .mat-slider-thumb, .mat-accent .mat-slider-thumb-label { - background-color: #797979; -} + background-color: #797979; } .mat-accent .mat-slider-thumb-label-text { - color: white; -} + color: white; } .mat-accent .mat-slider-focus-ring { - background-color: rgba(121, 121, 121, 0.2); -} + background-color: rgba(121, 121, 121, 0.2); } .mat-warn .mat-slider-track-fill, .mat-warn .mat-slider-thumb, .mat-warn .mat-slider-thumb-label { - background-color: red; -} + background-color: red; } .mat-warn .mat-slider-thumb-label-text { - color: white; -} + color: white; } .mat-warn .mat-slider-focus-ring { - background-color: rgba(255, 0, 0, 0.2); -} + background-color: rgba(255, 0, 0, 0.2); } .mat-slider:hover .mat-slider-track-background, .cdk-focused .mat-slider-track-background { - background-color: rgba(255, 255, 255, 0.3); -} + background-color: rgba(255, 255, 255, 0.3); } .mat-slider-disabled .mat-slider-track-background, .mat-slider-disabled .mat-slider-track-fill, .mat-slider-disabled .mat-slider-thumb { - background-color: rgba(255, 255, 255, 0.3); -} + background-color: rgba(255, 255, 255, 0.3); } .mat-slider-disabled:hover .mat-slider-track-background { - background-color: rgba(255, 255, 255, 0.3); -} + background-color: rgba(255, 255, 255, 0.3); } .mat-slider-min-value .mat-slider-focus-ring { - background-color: rgba(255, 255, 255, 0.12); -} + background-color: rgba(255, 255, 255, 0.12); } .mat-slider-min-value.mat-slider-thumb-label-showing .mat-slider-thumb, .mat-slider-min-value.mat-slider-thumb-label-showing .mat-slider-thumb-label { - background-color: white; -} + background-color: white; } .mat-slider-min-value.mat-slider-thumb-label-showing.cdk-focused .mat-slider-thumb, .mat-slider-min-value.mat-slider-thumb-label-showing.cdk-focused .mat-slider-thumb-label { - background-color: rgba(255, 255, 255, 0.3); -} + background-color: rgba(255, 255, 255, 0.3); } .mat-slider-min-value:not(.mat-slider-thumb-label-showing) .mat-slider-thumb { border-color: rgba(255, 255, 255, 0.3); - background-color: transparent; -} + background-color: transparent; } .mat-slider-min-value:not(.mat-slider-thumb-label-showing):hover .mat-slider-thumb, .mat-slider-min-value:not(.mat-slider-thumb-label-showing).cdk-focused .mat-slider-thumb { - border-color: rgba(255, 255, 255, 0.3); -} + border-color: rgba(255, 255, 255, 0.3); } .mat-slider-min-value:not(.mat-slider-thumb-label-showing):hover.mat-slider-disabled .mat-slider-thumb, .mat-slider-min-value:not(.mat-slider-thumb-label-showing).cdk-focused.mat-slider-disabled .mat-slider-thumb { - border-color: rgba(255, 255, 255, 0.3); -} + border-color: rgba(255, 255, 255, 0.3); } .mat-slider-has-ticks .mat-slider-wrapper::after { - border-color: rgba(255, 255, 255, 0.7); -} + border-color: rgba(255, 255, 255, 0.7); } .mat-slider-horizontal .mat-slider-ticks { background-image: repeating-linear-gradient(to right, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7) 2px, transparent 0, transparent); - background-image: -moz-repeating-linear-gradient(0.0001deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7) 2px, transparent 0, transparent); -} + background-image: -moz-repeating-linear-gradient(0.0001deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7) 2px, transparent 0, transparent); } .mat-slider-vertical .mat-slider-ticks { - background-image: repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7) 2px, transparent 0, transparent); -} + background-image: repeating-linear-gradient(to bottom, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.7) 2px, transparent 0, transparent); } .mat-step-header.cdk-keyboard-focused, .mat-step-header.cdk-program-focused, .mat-step-header:hover { - background-color: rgba(250, 250, 250, 0.04); -} + background-color: rgba(250, 250, 250, 0.04); } @media (hover: none) { .mat-step-header:hover { - background: none; - } -} + background: none; } } .mat-step-header .mat-step-label, .mat-step-header .mat-step-optional { - color: rgba(255, 255, 255, 0.7); -} + color: rgba(255, 255, 255, 0.7); } .mat-step-header .mat-step-icon { background-color: rgba(255, 255, 255, 0.7); - color: white; -} + color: rgba(18, 18, 18, 0.87); } .mat-step-header .mat-step-icon-selected, .mat-step-header .mat-step-icon-state-done, .mat-step-header .mat-step-icon-state-edit { - background-color: #3358cc; - color: white; -} + background-color: #9fa8da; + color: rgba(18, 18, 18, 0.87); } .mat-step-header.mat-accent .mat-step-icon { - color: white; -} + color: white; } .mat-step-header.mat-accent .mat-step-icon-selected, .mat-step-header.mat-accent .mat-step-icon-state-done, .mat-step-header.mat-accent .mat-step-icon-state-edit { background-color: #797979; - color: white; -} + color: white; } .mat-step-header.mat-warn .mat-step-icon { - color: white; -} + color: white; } .mat-step-header.mat-warn .mat-step-icon-selected, .mat-step-header.mat-warn .mat-step-icon-state-done, .mat-step-header.mat-warn .mat-step-icon-state-edit { background-color: red; - color: white; -} + color: white; } .mat-step-header .mat-step-icon-state-error { background-color: transparent; - color: red; -} + color: red; } .mat-step-header .mat-step-label.mat-step-label-active { - color: white; -} + color: white; } .mat-step-header .mat-step-label.mat-step-label-error { - color: red; -} + color: red; } .mat-stepper-horizontal, .mat-stepper-vertical { - background-color: #393939; -} + background-color: #2b2b2b; } .mat-stepper-vertical-line::before { - border-left-color: rgba(255, 255, 255, 0.12); -} + border-left-color: rgba(255, 255, 255, 0.12); } .mat-horizontal-stepper-header::before, .mat-horizontal-stepper-header::after, .mat-stepper-horizontal-line { - border-top-color: rgba(255, 255, 255, 0.12); -} + border-top-color: rgba(255, 255, 255, 0.12); } .mat-horizontal-stepper-header { - height: 72px; -} + height: 72px; } .mat-stepper-label-position-bottom .mat-horizontal-stepper-header, .mat-vertical-stepper-header { - padding: 24px 24px; -} + padding: 24px 24px; } .mat-stepper-vertical-line::before { top: -16px; - bottom: -16px; -} + bottom: -16px; } .mat-stepper-label-position-bottom .mat-horizontal-stepper-header::after, .mat-stepper-label-position-bottom .mat-horizontal-stepper-header::before { - top: 36px; -} + top: 36px; } .mat-stepper-label-position-bottom .mat-stepper-horizontal-line { - top: 36px; -} + top: 36px; } .mat-sort-header-arrow { - color: #c4c4c4; -} + color: #bfbfbf; } .mat-tab-nav-bar, .mat-tab-header { - border-bottom: 1px solid rgba(255, 255, 255, 0.12); -} + border-bottom: 1px solid rgba(255, 255, 255, 0.12); } .mat-tab-group-inverted-header .mat-tab-nav-bar, .mat-tab-group-inverted-header .mat-tab-header { border-top: 1px solid rgba(255, 255, 255, 0.12); - border-bottom: none; -} + border-bottom: none; } .mat-tab-label, .mat-tab-link { - color: white; -} - + color: white; } .mat-tab-label.mat-tab-disabled, .mat-tab-link.mat-tab-disabled { - color: rgba(255, 255, 255, 0.5); -} + color: rgba(255, 255, 255, 0.5); } .mat-tab-header-pagination-chevron { - border-color: white; -} + border-color: white; } .mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron { - border-color: rgba(255, 255, 255, 0.5); -} + border-color: rgba(255, 255, 255, 0.5); } .mat-tab-group[class*='mat-background-'] .mat-tab-header, .mat-tab-nav-bar[class*='mat-background-'] { border-bottom: none; - border-top: none; -} + border-top: none; } .mat-tab-group.mat-primary .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-primary .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-group.mat-primary .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-primary .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-primary .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-primary .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-primary .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-primary .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled) { - background-color: rgba(194, 205, 240, 0.3); -} + background-color: rgba(226, 229, 244, 0.3); } .mat-tab-group.mat-primary .mat-ink-bar, .mat-tab-nav-bar.mat-primary .mat-ink-bar { - background-color: #3358cc; -} + background-color: #9fa8da; } .mat-tab-group.mat-primary.mat-background-primary > .mat-tab-header .mat-ink-bar, .mat-tab-group.mat-primary.mat-background-primary > .mat-tab-link-container .mat-ink-bar, .mat-tab-nav-bar.mat-primary.mat-background-primary > .mat-tab-header .mat-ink-bar, .mat-tab-nav-bar.mat-primary.mat-background-primary > .mat-tab-link-container .mat-ink-bar { - background-color: white; -} + background-color: rgba(18, 18, 18, 0.87); } .mat-tab-group.mat-accent .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-accent .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-group.mat-accent .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-accent .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-accent .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-accent .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-accent .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-accent .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled) { - background-color: rgba(215, 215, 215, 0.3); -} + background-color: rgba(215, 215, 215, 0.3); } .mat-tab-group.mat-accent .mat-ink-bar, .mat-tab-nav-bar.mat-accent .mat-ink-bar { - background-color: #797979; -} + background-color: #797979; } .mat-tab-group.mat-accent.mat-background-accent > .mat-tab-header .mat-ink-bar, .mat-tab-group.mat-accent.mat-background-accent > .mat-tab-link-container .mat-ink-bar, .mat-tab-nav-bar.mat-accent.mat-background-accent > .mat-tab-header .mat-ink-bar, .mat-tab-nav-bar.mat-accent.mat-background-accent > .mat-tab-link-container .mat-ink-bar { - background-color: white; -} + background-color: white; } .mat-tab-group.mat-warn .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-warn .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-group.mat-warn .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-warn .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-warn .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-warn .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-warn .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-warn .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled) { - background-color: rgba(255, 179, 179, 0.3); -} + background-color: rgba(255, 179, 179, 0.3); } .mat-tab-group.mat-warn .mat-ink-bar, .mat-tab-nav-bar.mat-warn .mat-ink-bar { - background-color: red; -} + background-color: red; } .mat-tab-group.mat-warn.mat-background-warn > .mat-tab-header .mat-ink-bar, .mat-tab-group.mat-warn.mat-background-warn > .mat-tab-link-container .mat-ink-bar, .mat-tab-nav-bar.mat-warn.mat-background-warn > .mat-tab-header .mat-ink-bar, .mat-tab-nav-bar.mat-warn.mat-background-warn > .mat-tab-link-container .mat-ink-bar { - background-color: white; -} + background-color: white; } .mat-tab-group.mat-background-primary .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-background-primary .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-group.mat-background-primary .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-background-primary .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-primary .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-primary .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-primary .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-primary .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled) { - background-color: rgba(194, 205, 240, 0.3); -} + background-color: rgba(226, 229, 244, 0.3); } .mat-tab-group.mat-background-primary > .mat-tab-header, .mat-tab-group.mat-background-primary > .mat-tab-link-container, .mat-tab-group.mat-background-primary > .mat-tab-header-pagination, .mat-tab-nav-bar.mat-background-primary > .mat-tab-header, .mat-tab-nav-bar.mat-background-primary > .mat-tab-link-container, .mat-tab-nav-bar.mat-background-primary > .mat-tab-header-pagination { - background-color: #3358cc; -} + background-color: #9fa8da; } .mat-tab-group.mat-background-primary > .mat-tab-header .mat-tab-label, .mat-tab-group.mat-background-primary > .mat-tab-link-container .mat-tab-link, .mat-tab-nav-bar.mat-background-primary > .mat-tab-header .mat-tab-label, .mat-tab-nav-bar.mat-background-primary > .mat-tab-link-container .mat-tab-link { - color: white; -} - + color: rgba(18, 18, 18, 0.87); } .mat-tab-group.mat-background-primary > .mat-tab-header .mat-tab-label.mat-tab-disabled, .mat-tab-group.mat-background-primary > .mat-tab-link-container .mat-tab-link.mat-tab-disabled, .mat-tab-nav-bar.mat-background-primary > .mat-tab-header .mat-tab-label.mat-tab-disabled, .mat-tab-nav-bar.mat-background-primary > .mat-tab-link-container .mat-tab-link.mat-tab-disabled { - color: rgba(255, 255, 255, 0.4); -} + color: rgba(18, 18, 18, 0.4); } .mat-tab-group.mat-background-primary > .mat-tab-header-pagination .mat-tab-header-pagination-chevron, .mat-tab-group.mat-background-primary > .mat-tab-links .mat-focus-indicator::before, .mat-tab-group.mat-background-primary > .mat-tab-header .mat-focus-indicator::before, .mat-tab-nav-bar.mat-background-primary > .mat-tab-header-pagination .mat-tab-header-pagination-chevron, .mat-tab-nav-bar.mat-background-primary > .mat-tab-links .mat-focus-indicator::before, .mat-tab-nav-bar.mat-background-primary > .mat-tab-header .mat-focus-indicator::before { - border-color: white; -} + border-color: rgba(18, 18, 18, 0.87); } .mat-tab-group.mat-background-primary > .mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron, .mat-tab-nav-bar.mat-background-primary > .mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron { - border-color: rgba(255, 255, 255, 0.4); -} + border-color: rgba(18, 18, 18, 0.4); } .mat-tab-group.mat-background-primary > .mat-tab-header .mat-ripple-element, .mat-tab-group.mat-background-primary > .mat-tab-link-container .mat-ripple-element, .mat-tab-nav-bar.mat-background-primary > .mat-tab-header .mat-ripple-element, .mat-tab-nav-bar.mat-background-primary > .mat-tab-link-container .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.12); -} + background-color: rgba(18, 18, 18, 0.12); } .mat-tab-group.mat-background-accent .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-background-accent .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-group.mat-background-accent .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-background-accent .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-accent .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-accent .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-accent .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-accent .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled) { - background-color: rgba(215, 215, 215, 0.3); -} + background-color: rgba(215, 215, 215, 0.3); } .mat-tab-group.mat-background-accent > .mat-tab-header, .mat-tab-group.mat-background-accent > .mat-tab-link-container, .mat-tab-group.mat-background-accent > .mat-tab-header-pagination, .mat-tab-nav-bar.mat-background-accent > .mat-tab-header, .mat-tab-nav-bar.mat-background-accent > .mat-tab-link-container, .mat-tab-nav-bar.mat-background-accent > .mat-tab-header-pagination { - background-color: #797979; -} + background-color: #797979; } .mat-tab-group.mat-background-accent > .mat-tab-header .mat-tab-label, .mat-tab-group.mat-background-accent > .mat-tab-link-container .mat-tab-link, .mat-tab-nav-bar.mat-background-accent > .mat-tab-header .mat-tab-label, .mat-tab-nav-bar.mat-background-accent > .mat-tab-link-container .mat-tab-link { - color: white; -} - + color: white; } .mat-tab-group.mat-background-accent > .mat-tab-header .mat-tab-label.mat-tab-disabled, .mat-tab-group.mat-background-accent > .mat-tab-link-container .mat-tab-link.mat-tab-disabled, .mat-tab-nav-bar.mat-background-accent > .mat-tab-header .mat-tab-label.mat-tab-disabled, .mat-tab-nav-bar.mat-background-accent > .mat-tab-link-container .mat-tab-link.mat-tab-disabled { - color: rgba(255, 255, 255, 0.4); -} + color: rgba(255, 255, 255, 0.4); } .mat-tab-group.mat-background-accent > .mat-tab-header-pagination .mat-tab-header-pagination-chevron, .mat-tab-group.mat-background-accent > .mat-tab-links .mat-focus-indicator::before, .mat-tab-group.mat-background-accent > .mat-tab-header .mat-focus-indicator::before, .mat-tab-nav-bar.mat-background-accent > .mat-tab-header-pagination .mat-tab-header-pagination-chevron, .mat-tab-nav-bar.mat-background-accent > .mat-tab-links .mat-focus-indicator::before, .mat-tab-nav-bar.mat-background-accent > .mat-tab-header .mat-focus-indicator::before { - border-color: white; -} + border-color: white; } .mat-tab-group.mat-background-accent > .mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron, .mat-tab-nav-bar.mat-background-accent > .mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron { - border-color: rgba(255, 255, 255, 0.4); -} + border-color: rgba(255, 255, 255, 0.4); } .mat-tab-group.mat-background-accent > .mat-tab-header .mat-ripple-element, .mat-tab-group.mat-background-accent > .mat-tab-link-container .mat-ripple-element, .mat-tab-nav-bar.mat-background-accent > .mat-tab-header .mat-ripple-element, .mat-tab-nav-bar.mat-background-accent > .mat-tab-link-container .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.12); -} + background-color: rgba(255, 255, 255, 0.12); } .mat-tab-group.mat-background-warn .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-background-warn .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-group.mat-background-warn .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-group.mat-background-warn .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-warn .mat-tab-label.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-warn .mat-tab-label.cdk-program-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-warn .mat-tab-link.cdk-keyboard-focused:not(.mat-tab-disabled), .mat-tab-nav-bar.mat-background-warn .mat-tab-link.cdk-program-focused:not(.mat-tab-disabled) { - background-color: rgba(255, 179, 179, 0.3); -} + background-color: rgba(255, 179, 179, 0.3); } .mat-tab-group.mat-background-warn > .mat-tab-header, .mat-tab-group.mat-background-warn > .mat-tab-link-container, .mat-tab-group.mat-background-warn > .mat-tab-header-pagination, .mat-tab-nav-bar.mat-background-warn > .mat-tab-header, .mat-tab-nav-bar.mat-background-warn > .mat-tab-link-container, .mat-tab-nav-bar.mat-background-warn > .mat-tab-header-pagination { - background-color: red; -} + background-color: red; } .mat-tab-group.mat-background-warn > .mat-tab-header .mat-tab-label, .mat-tab-group.mat-background-warn > .mat-tab-link-container .mat-tab-link, .mat-tab-nav-bar.mat-background-warn > .mat-tab-header .mat-tab-label, .mat-tab-nav-bar.mat-background-warn > .mat-tab-link-container .mat-tab-link { - color: white; -} - + color: white; } .mat-tab-group.mat-background-warn > .mat-tab-header .mat-tab-label.mat-tab-disabled, .mat-tab-group.mat-background-warn > .mat-tab-link-container .mat-tab-link.mat-tab-disabled, .mat-tab-nav-bar.mat-background-warn > .mat-tab-header .mat-tab-label.mat-tab-disabled, .mat-tab-nav-bar.mat-background-warn > .mat-tab-link-container .mat-tab-link.mat-tab-disabled { - color: rgba(255, 255, 255, 0.4); -} + color: rgba(255, 255, 255, 0.4); } .mat-tab-group.mat-background-warn > .mat-tab-header-pagination .mat-tab-header-pagination-chevron, .mat-tab-group.mat-background-warn > .mat-tab-links .mat-focus-indicator::before, .mat-tab-group.mat-background-warn > .mat-tab-header .mat-focus-indicator::before, .mat-tab-nav-bar.mat-background-warn > .mat-tab-header-pagination .mat-tab-header-pagination-chevron, .mat-tab-nav-bar.mat-background-warn > .mat-tab-links .mat-focus-indicator::before, .mat-tab-nav-bar.mat-background-warn > .mat-tab-header .mat-focus-indicator::before { - border-color: white; -} + border-color: white; } .mat-tab-group.mat-background-warn > .mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron, .mat-tab-nav-bar.mat-background-warn > .mat-tab-header-pagination-disabled .mat-tab-header-pagination-chevron { - border-color: rgba(255, 255, 255, 0.4); -} + border-color: rgba(255, 255, 255, 0.4); } .mat-tab-group.mat-background-warn > .mat-tab-header .mat-ripple-element, .mat-tab-group.mat-background-warn > .mat-tab-link-container .mat-ripple-element, .mat-tab-nav-bar.mat-background-warn > .mat-tab-header .mat-ripple-element, .mat-tab-nav-bar.mat-background-warn > .mat-tab-link-container .mat-ripple-element { - background-color: rgba(255, 255, 255, 0.12); -} + background-color: rgba(255, 255, 255, 0.12); } .mat-toolbar { - background: #393939; - color: white; -} - + background: #2b2b2b; + color: white; } .mat-toolbar.mat-primary { - background: #3358cc; - color: white; -} - + background: #9fa8da; + color: rgba(18, 18, 18, 0.87); } .mat-toolbar.mat-accent { background: #797979; - color: white; -} - + color: white; } .mat-toolbar.mat-warn { background: red; - color: white; -} - + color: white; } .mat-toolbar .mat-form-field-underline, .mat-toolbar .mat-form-field-ripple, .mat-toolbar .mat-focused .mat-form-field-ripple { - background-color: currentColor; -} - + background-color: currentColor; } .mat-toolbar .mat-form-field-label, .mat-toolbar .mat-focused .mat-form-field-label, .mat-toolbar .mat-select-value, .mat-toolbar .mat-select-arrow, .mat-toolbar .mat-form-field.mat-focused .mat-select-arrow { - color: inherit; -} - + color: inherit; } .mat-toolbar .mat-input-element { - caret-color: currentColor; -} + caret-color: currentColor; } .mat-toolbar-multiple-rows { - min-height: 64px; -} + min-height: 64px; } .mat-toolbar-row, .mat-toolbar-single-row { - height: 64px; -} + height: 64px; } @media (max-width: 599px) { .mat-toolbar-multiple-rows { - min-height: 56px; - } - + min-height: 56px; } .mat-toolbar-row, .mat-toolbar-single-row { - height: 56px; - } -} + height: 56px; } } .mat-tooltip { - background: rgba(95, 95, 95, 0.9); -} + background: rgba(81, 81, 81, 0.9); } .mat-tree { - background: #393939; -} + background: #2b2b2b; } .mat-tree-node, .mat-nested-tree-node { - color: white; -} + color: white; } .mat-tree-node { - min-height: 48px; -} + min-height: 48px; } .mat-snack-bar-container { - color: rgba(0, 0, 0, 0.87); + color: rgba(18, 18, 18, 0.87); background: #fafafa; - box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); -} + box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12); } .mat-simple-snackbar-action { - color: inherit; -} + color: inherit; } .mat-raised-button, .mat-stroked-button, .mat-flat-button { padding: 0 1.15em; margin: 0 .65em; min-width: 3em; - line-height: 36.4px; -} + line-height: 36.4px; } .mat-standard-chip { padding: .5em .85em; - min-height: 2.5em; -} + min-height: 2.5em; } .material-icons { font-size: 24px; - font-family: 'Material Icons', 'Material Icons'; -} - + font-family: 'Material Icons', 'Material Icons'; } .material-icons .mat-badge-content { - font-family: 'Roboto'; -} + font-family: 'Roboto'; } From 428295cfa3d72a3e11a94edb4556046039d99eb3 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Fri, 26 Nov 2021 12:42:35 +0100 Subject: [PATCH 03/25] fix: changed frontend routing --- server/src/app.module.ts | 11 ++++--- server/src/middleware/frontend.middleware.ts | 32 ++++++++++++++++++++ server/src/static/static.module.ts | 24 --------------- 3 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 server/src/middleware/frontend.middleware.ts delete mode 100644 server/src/static/static.module.ts diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 8d44d24..543e749 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,6 +1,5 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import { ConfigModule } from './config/config.module'; -import { StaticModule } from './static/static.module'; import { ArgsModule } from './args/args.module'; import { UserModule } from './user/user.module'; import { LoginModule } from './login/login.module'; @@ -9,6 +8,7 @@ import { TokenModule } from './token/token.module'; import { AdminModule } from './admin/admin.module'; import { CharacterModule } from './character/character.module'; import { PluginModule } from './plugin/plugin.module'; +import { FrontendMiddleware } from './middleware/frontend.middleware'; @Module({ imports: [ @@ -16,7 +16,6 @@ import { PluginModule } from './plugin/plugin.module'; ConfigModule, ArgsModule, DatabaseModule, - StaticModule, UserModule, LoginModule, AdminModule, @@ -24,4 +23,8 @@ import { PluginModule } from './plugin/plugin.module'; PluginModule, ], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer): any { + consumer.apply(FrontendMiddleware).forRoutes({ path: '/**', method: RequestMethod.ALL }); + } +} diff --git a/server/src/middleware/frontend.middleware.ts b/server/src/middleware/frontend.middleware.ts new file mode 100644 index 0000000..22764be --- /dev/null +++ b/server/src/middleware/frontend.middleware.ts @@ -0,0 +1,32 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response } from 'express'; +import { join, resolve } from 'path'; +import { Args, ArgsService } from '../args/args.service'; + +const allowedExt = ['.js', '.ico', '.css', '.png', '.jpg', '.woff2', '.woff', '.ttf', '.svg', '.json']; + +@Injectable() +export class FrontendMiddleware implements NestMiddleware { + private readonly dataPath: string; + + constructor(argsService: ArgsService) { + this.dataPath = argsService.getArgument(Args.DATAPATH); + } + + use(req: Request, res: Response, next: () => void): void { + const { url } = req; + if (url.indexOf('api') === 1) { + next(); + return; + } + if (url.indexOf('plugins') === 1) { + res.sendFile(join(resolve(this.dataPath), url)); + return; + } + if (allowedExt.filter((ext) => url.indexOf(ext) > 0).length > 0) { + res.sendFile(join(process.cwd(), 'dist', 'app', url)); + return; + } + res.sendFile(join(process.cwd(), 'dist', 'app', 'index.html')); + } +} diff --git a/server/src/static/static.module.ts b/server/src/static/static.module.ts deleted file mode 100644 index 92b532e..0000000 --- a/server/src/static/static.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ServeStaticModule } from '@nestjs/serve-static'; -import path from 'path'; -import { Args, ArgsService } from '../args/args.service'; - -const dataPath = ArgsService.get().getArgument(Args.DATAPATH); - -@Module({ - imports: [ - ServeStaticModule.forRoot( - { - rootPath: path.join(dataPath, 'plugins'), - serveRoot: '/plugins', - }, - { - rootPath: path.join(process.cwd(), 'dist', 'app'), - serveRoot: '/', - }, - ), - ], - controllers: [], - providers: [], -}) -export class StaticModule {} From 5ad9ae50f512dbc46136fd1fa7fd708d3e2a37dc Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Fri, 26 Nov 2021 15:32:00 +0100 Subject: [PATCH 04/25] feat: added expedition planning as beta feature (not finished!) --- libs/model/src/calendar.model.ts | 1 + libs/model/src/expedition.model.ts | 20 ++++++ libs/model/src/index.ts | 2 + webapp/src/app/app-routing.module.ts | 4 ++ webapp/src/app/app.module.ts | 6 +- .../expedition-create.component.css | 0 .../expedition-create.component.html | 28 ++++++++ .../expedition-create.component.spec.ts | 25 ++++++++ .../expedition-create.component.ts | 64 +++++++++++++++++++ .../expedition-table.component.css | 0 .../expedition-table.component.html | 43 +++++++++++++ .../expedition-table.component.spec.ts | 25 ++++++++ .../expedition-table.component.ts | 33 ++++++++++ .../expedition/expedition-page.module.ts | 37 +++++++++++ .../expedition/expedition-routing.module.ts | 16 +++++ .../routes/root/expedition.component.css | 0 .../routes/root/expedition.component.html | 8 +++ .../routes/root/expedition.component.ts | 8 +++ .../home-calendar/home-calendar.component.ts | 28 +++++++- .../services/expedition/expedition.module.ts | 7 ++ .../services/expedition/expedition.service.ts | 25 ++++++++ .../services/navigation/navigation.service.ts | 5 ++ webapp/src/styles.css | 8 +++ 23 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 libs/model/src/calendar.model.ts create mode 100644 libs/model/src/expedition.model.ts create mode 100644 webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.css create mode 100644 webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.html create mode 100644 webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.spec.ts create mode 100644 webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts create mode 100644 webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.css create mode 100644 webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.html create mode 100644 webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.spec.ts create mode 100644 webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts create mode 100644 webapp/src/app/pages/expedition/expedition-page.module.ts create mode 100644 webapp/src/app/pages/expedition/expedition-routing.module.ts create mode 100644 webapp/src/app/pages/expedition/routes/root/expedition.component.css create mode 100644 webapp/src/app/pages/expedition/routes/root/expedition.component.html create mode 100644 webapp/src/app/pages/expedition/routes/root/expedition.component.ts create mode 100644 webapp/src/app/services/expedition/expedition.module.ts create mode 100644 webapp/src/app/services/expedition/expedition.service.ts diff --git a/libs/model/src/calendar.model.ts b/libs/model/src/calendar.model.ts new file mode 100644 index 0000000..9ade4cd --- /dev/null +++ b/libs/model/src/calendar.model.ts @@ -0,0 +1 @@ +export type CalendarEvent = {}; diff --git a/libs/model/src/expedition.model.ts b/libs/model/src/expedition.model.ts new file mode 100644 index 0000000..08fec85 --- /dev/null +++ b/libs/model/src/expedition.model.ts @@ -0,0 +1,20 @@ +export type Expedition = { + name: string; + beginDateTime: string; + participants: Participant[]; + owner: Owner; +}; + +export type Participant = { + userId: number; + characterName: string; + discordId: string; + role: 'tank' | 'damage' | 'heal'; + hasKey: boolean; +}; + +export type Owner = { + userId: number; + characterName: string; + discordId: string; +}; diff --git a/libs/model/src/index.ts b/libs/model/src/index.ts index 74611e1..dc30237 100644 --- a/libs/model/src/index.ts +++ b/libs/model/src/index.ts @@ -1,7 +1,9 @@ export * from './admin.model'; +export * from './calendar.model'; export * from './character.model'; export * from './config.model'; export * from './discord.model'; +export * from './expedition.model'; export * from './github.model'; export * from './login.model'; export * from './plugin.model'; diff --git a/webapp/src/app/app-routing.module.ts b/webapp/src/app/app-routing.module.ts index f8cf120..86156ef 100644 --- a/webapp/src/app/app-routing.module.ts +++ b/webapp/src/app/app-routing.module.ts @@ -31,6 +31,10 @@ export const routes: Routes = [ { path: 'my-character', loadChildren: () => import('./pages/my-character/my-character-page.module').then((m) => m.MyCharacterPageModule) + }, + { + path: 'expedition', + loadChildren: () => import('./pages/expedition/expedition-page.module').then((m) => m.ExpeditionPageModule) } ]; diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts index 8301551..64681da 100644 --- a/webapp/src/app/app.module.ts +++ b/webapp/src/app/app.module.ts @@ -1,4 +1,4 @@ -import { NgModule } from '@angular/core'; +import { LOCALE_ID, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; @@ -31,6 +31,7 @@ import { FullCalendarModule } from '@fullcalendar/angular'; import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; import interactionPlugin from '@fullcalendar/interaction'; +import { ExpeditionModule } from './services/expedition/expedition.module'; const cookieConfig: NgcCookieConsentConfig = { cookie: { @@ -79,6 +80,7 @@ FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPl AdminModule, CharacterModule, ConfigModule, + ExpeditionModule, NavigationModule, PluginModule, SnackbarModule, @@ -90,7 +92,7 @@ FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPl MatButtonModule, MatMomentDateModule ], - providers: [AppComponent, LoginGuard, AdminGuard], + providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }, AppComponent, LoginGuard, AdminGuard], bootstrap: [AppComponent], exports: [] }) diff --git a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.css b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.css new file mode 100644 index 0000000..e69de29 diff --git a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.html b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.html new file mode 100644 index 0000000..0fbefd1 --- /dev/null +++ b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.html @@ -0,0 +1,28 @@ +
+
+ + Date + + + + + + Time + + + + Expedition + + Expedition 1 + Expedition 2 + + + I have a tuning orb +
+ +
+ + +
+ +
diff --git a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.spec.ts b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.spec.ts new file mode 100644 index 0000000..8ef876a --- /dev/null +++ b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExpeditionCreateComponent } from './expedition-create.component'; + +describe('ExpeditionCreateComponent', () => { + let component: ExpeditionCreateComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ExpeditionCreateComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExpeditionCreateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts new file mode 100644 index 0000000..840a6e2 --- /dev/null +++ b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts @@ -0,0 +1,64 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import * as moment from 'moment'; +import { Moment } from 'moment'; +import { ExpeditionService } from '../../../../services/expedition/expedition.service'; +import { MatDialogRef } from '@angular/material/dialog'; + +type CreateExpeditionForm = { + date: Moment; + time: string; + expedition: string; + hasKey: boolean; +}; + +@Component({ + selector: 'app-expedition-create', + templateUrl: './expedition-create.component.html', + styleUrls: ['./expedition-create.component.css'] +}) +export class ExpeditionCreateComponent { + form = new FormGroup({ + date: new FormControl(moment(), [Validators.required]), + time: new FormControl(moment().format('HH:mm'), [Validators.required]), + expedition: new FormControl(null, [Validators.required]), + hasKey: new FormControl(false) + }); + + constructor( + public dialogRef: MatDialogRef, + private expeditionService: ExpeditionService + ) {} + + create() { + this.form.markAllAsTouched(); + const expedition: CreateExpeditionForm = this.form.value; + + if (this.form.valid) { + this.expeditionService.createExpedition({ + name: expedition.expedition, + beginDateTime: expedition.date + .set({ + hour: +expedition.time.split(':')[0], + minute: +expedition.time.split(':')[1] + }) + .format('YYYY-MM-DDTHH:mm'), + participants: [ + { + userId: 1, + characterName: 'Krise', + discordId: '1', + hasKey: true, + role: 'damage' + } + ], + owner: { + userId: 1, + characterName: 'Krise', + discordId: '1' + } + }); + this.dialogRef.close(); + } + } +} diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.css b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.css new file mode 100644 index 0000000..e69de29 diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.html b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.html new file mode 100644 index 0000000..b7d47b0 --- /dev/null +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.html @@ -0,0 +1,43 @@ +
+ +
+
+ + + + Begin + + + + + + Name + {{row.name}} + + + + + Owner + {{row.owner.characterName}} + + + + + Participants + + + + + + + + + + + + + +
diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.spec.ts b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.spec.ts new file mode 100644 index 0000000..3040e03 --- /dev/null +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExpeditionTableComponent } from './expedition-table.component'; + +describe('ExpeditionTableComponent', () => { + let component: ExpeditionTableComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ExpeditionTableComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExpeditionTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts new file mode 100644 index 0000000..27ec2a0 --- /dev/null +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts @@ -0,0 +1,33 @@ +import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatTableDataSource } from '@angular/material/table'; +import { Expedition } from '@nw-company-tool/model'; +import { ExpeditionService } from '../../../../services/expedition/expedition.service'; +import { MatDialog } from '@angular/material/dialog'; +import { ExpeditionCreateComponent } from '../expedition-create/expedition-create.component'; + +@Component({ + selector: 'app-expedition-table', + templateUrl: './expedition-table.component.html', + styleUrls: ['./expedition-table.component.css'] +}) +export class ExpeditionTableComponent implements OnInit, AfterViewInit { + displayedColumns: string[] = ['planned', 'name', 'owner', 'participants']; + dataSource = new MatTableDataSource(); + + @ViewChild(MatPaginator) paginator: MatPaginator; + + constructor(private expeditionService: ExpeditionService, public dialog: MatDialog) {} + + ngOnInit(): void { + this.expeditionService.getExpeditions().subscribe((data) => (this.dataSource.data = data)); + } + + ngAfterViewInit(): void { + this.dataSource.paginator = this.paginator; + } + + createNew(): void { + this.dialog.open(ExpeditionCreateComponent); + } +} diff --git a/webapp/src/app/pages/expedition/expedition-page.module.ts b/webapp/src/app/pages/expedition/expedition-page.module.ts new file mode 100644 index 0000000..287013b --- /dev/null +++ b/webapp/src/app/pages/expedition/expedition-page.module.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { ExpeditionRoutingModule } from './expedition-routing.module'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { ExpeditionComponent } from './routes/root/expedition.component'; +import { ExpeditionTableComponent } from './components/expedition-table/expedition-table.component'; +import { MatTableModule } from '@angular/material/table'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatInputModule } from '@angular/material/input'; +import { ExpeditionCreateComponent } from './components/expedition-create/expedition-create.component'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatSelectModule } from '@angular/material/select'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { ReactiveFormsModule } from '@angular/forms'; + +@NgModule({ + imports: [ + ExpeditionRoutingModule, + MatCardModule, + MatButtonModule, + MatTableModule, + MatPaginatorModule, + MatInputModule, + MatIconModule, + MatDialogModule, + MatSelectModule, + MatDatepickerModule, + MatSlideToggleModule, + ReactiveFormsModule + ], + declarations: [ExpeditionComponent, ExpeditionTableComponent, ExpeditionCreateComponent], + providers: [], + exports: [] +}) +export class ExpeditionPageModule {} diff --git a/webapp/src/app/pages/expedition/expedition-routing.module.ts b/webapp/src/app/pages/expedition/expedition-routing.module.ts new file mode 100644 index 0000000..f544093 --- /dev/null +++ b/webapp/src/app/pages/expedition/expedition-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { ExpeditionComponent } from './routes/root/expedition.component'; + +const routes: Routes = [ + { + path: '', + component: ExpeditionComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ExpeditionRoutingModule {} diff --git a/webapp/src/app/pages/expedition/routes/root/expedition.component.css b/webapp/src/app/pages/expedition/routes/root/expedition.component.css new file mode 100644 index 0000000..e69de29 diff --git a/webapp/src/app/pages/expedition/routes/root/expedition.component.html b/webapp/src/app/pages/expedition/routes/root/expedition.component.html new file mode 100644 index 0000000..9bfaaec --- /dev/null +++ b/webapp/src/app/pages/expedition/routes/root/expedition.component.html @@ -0,0 +1,8 @@ +
+
+

🚧 UNDER CONSTRUCTION - ONLY USE FOR DEMO PURPOSES 🚧

+
+ + + +
diff --git a/webapp/src/app/pages/expedition/routes/root/expedition.component.ts b/webapp/src/app/pages/expedition/routes/root/expedition.component.ts new file mode 100644 index 0000000..4d0c6af --- /dev/null +++ b/webapp/src/app/pages/expedition/routes/root/expedition.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-expedition', + templateUrl: './expedition.component.html', + styleUrls: ['./expedition.component.css'] +}) +export class ExpeditionComponent {} diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts index bf12c08..2d5b53e 100644 --- a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts @@ -1,6 +1,8 @@ import { Component } from '@angular/core'; import { CalendarOptions } from '@fullcalendar/angular'; import * as moment from 'moment'; +import { ExpeditionService } from '../../../../services/expedition/expedition.service'; +import { map } from 'rxjs/operators'; @Component({ selector: 'app-home-calendar', @@ -21,11 +23,31 @@ export class HomeCalendarComponent { right: '' }, aspectRatio: 2, - editable: true, - selectable: true, + editable: false, + selectable: false, selectMirror: true, dayMaxEvents: true, firstDay: 1, - locale: navigator.language + locale: navigator.language, + eventSources: [ + { + color: 'yellow', + events: (info, success) => { + this.expeditionService + .getExpeditions() + .pipe( + map((expeditions) => + expeditions.map((expedition) => ({ + title: expedition.name, + start: expedition.beginDateTime + })) + ) + ) + .subscribe((events) => success(events)); + } + } + ] }; + + constructor(private expeditionService: ExpeditionService) {} } diff --git a/webapp/src/app/services/expedition/expedition.module.ts b/webapp/src/app/services/expedition/expedition.module.ts new file mode 100644 index 0000000..075b2ba --- /dev/null +++ b/webapp/src/app/services/expedition/expedition.module.ts @@ -0,0 +1,7 @@ +import { NgModule } from '@angular/core'; +import { ExpeditionService } from './expedition.service'; + +@NgModule({ + providers: [ExpeditionService] +}) +export class ExpeditionModule {} diff --git a/webapp/src/app/services/expedition/expedition.service.ts b/webapp/src/app/services/expedition/expedition.service.ts new file mode 100644 index 0000000..01682ab --- /dev/null +++ b/webapp/src/app/services/expedition/expedition.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { Expedition } from '@nw-company-tool/model'; +import { Observable, ReplaySubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class ExpeditionService { + private expeditions$ = new ReplaySubject(1); + + private expeditions: Expedition[] = []; + + constructor() { + this.expeditions$.next(this.expeditions); + } + + public getExpeditions(): Observable { + return this.expeditions$; + } + + public createExpedition(expedition: Expedition): void { + this.expeditions.push(expedition); + this.expeditions$.next(this.expeditions); + } +} diff --git a/webapp/src/app/services/navigation/navigation.service.ts b/webapp/src/app/services/navigation/navigation.service.ts index 96fa055..489b850 100644 --- a/webapp/src/app/services/navigation/navigation.service.ts +++ b/webapp/src/app/services/navigation/navigation.service.ts @@ -22,6 +22,11 @@ export class NavigationService { label: 'COMPANY', routerLink: 'company', icon: 'group' + }, + { + label: 'EXPEDITION', + routerLink: 'expedition', + icon: 'landscape' } ]; diff --git a/webapp/src/styles.css b/webapp/src/styles.css index e4a57b6..a889a54 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -48,6 +48,10 @@ input[type=number] { margin: 6px 18px 6px 18px; } +.vertical-margin { + margin: 18px 0 18px 0; +} + .flex { display: flex; } @@ -64,6 +68,10 @@ input[type=number] { justify-content: center; } +.flex-right { + justify-content: right; +} + .flex-vertical-center { align-items: center; } From 3d3095280f5fa45df041a6379b5d32ebaa045820 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Sun, 28 Nov 2021 18:07:34 +0100 Subject: [PATCH 05/25] fix: update did not output binary files correctly --- server/src/admin/admin.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/admin/admin.service.ts b/server/src/admin/admin.service.ts index bb7054b..019700d 100644 --- a/server/src/admin/admin.service.ts +++ b/server/src/admin/admin.service.ts @@ -71,18 +71,18 @@ export class AdminService { return new Promise((resolve) => { const extract = tar.extract(); extract.on('entry', (header, stream, next) => { - let data = ''; + const data = []; stream.on('data', (chunk) => { if (header.type === 'file') { - data += chunk; + data.push(chunk); } }); stream.on('end', () => { if (header.type === 'file') { const filePath = `${process.cwd()}/${header.name}`; - fs.outputFileSync(filePath, data); + fs.outputFileSync(filePath, Buffer.concat(data)); console.log(`updated: ${header.name}`); } next(); From 7df1e57bc65aff4c9b489e52c08d6d43eec6b5a2 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Sun, 28 Nov 2021 18:11:59 +0100 Subject: [PATCH 06/25] fix: company page did not show attributes (default) when first opening the dialog --- .../components/characters-table/characters-table.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts b/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts index 4087633..39b111b 100644 --- a/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts +++ b/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts @@ -60,7 +60,7 @@ export class CharactersTableComponent implements OnInit, AfterViewInit { ngOnInit(): void { this.characterService .query({ - attributes: BASE + attributes: BASE.concat(ATTRIBUTES) }) .subscribe((data) => { this.data = data.sort((a, b) => this.compare(a.characterName, b.characterName, true)); From 7c26f2bf9a1e010ac43a91b68f910896869a04eb Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Sun, 28 Nov 2021 18:16:18 +0100 Subject: [PATCH 07/25] fix: user could not be set as admins anymore --- server/src/admin/admin.controller.ts | 2 +- webapp/src/app/services/admin/admin.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/admin/admin.controller.ts b/server/src/admin/admin.controller.ts index 4652d98..a093407 100644 --- a/server/src/admin/admin.controller.ts +++ b/server/src/admin/admin.controller.ts @@ -26,7 +26,7 @@ export class AdminController { } @Post('/users/admin') - async setAdmin(@Param() params, @Body() body: AdminUserDto): Promise { + async setAdmin(@Body() body: AdminUserDto): Promise { if (body.admin) { await this.userService.setPermission(body.userId, Permission.ADMIN); } else { diff --git a/webapp/src/app/services/admin/admin.service.ts b/webapp/src/app/services/admin/admin.service.ts index 2cfd80f..8edd0e9 100644 --- a/webapp/src/app/services/admin/admin.service.ts +++ b/webapp/src/app/services/admin/admin.service.ts @@ -27,7 +27,7 @@ export class AdminService { userId: id, admin }; - return this.http.post(`/api/admin/users/admin/${id}`, payload, { withCredentials: true }); + return this.http.post(`/api/admin/users/admin`, payload, { withCredentials: true }); } public getLatestReleaseVersion(): Observable { From f59360752ea2b6f4d532995fc4e001c371dc5e6f Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Sun, 28 Nov 2021 19:11:44 +0100 Subject: [PATCH 08/25] fix: routing did not work correctly --- webapp/src/app/app.component.ts | 4 +++- webapp/src/app/pages/expedition/expedition-routing.module.ts | 4 +++- webapp/src/app/services/user/user.service.ts | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts index 18be43e..9c3dd29 100644 --- a/webapp/src/app/app.component.ts +++ b/webapp/src/app/app.component.ts @@ -4,6 +4,7 @@ import { Router, Routes } from '@angular/router'; import { PluginDefinition } from './services/plugin/plugin.model'; import { loadRemoteModule } from '@angular-architects/module-federation'; import { routes } from './app-routing.module'; +import { UserService } from './services/user/user.service'; @Component({ selector: 'app-root', @@ -11,12 +12,13 @@ import { routes } from './app-routing.module'; styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { - constructor(private router: Router, private pluginService: PluginService) {} + constructor(private userService: UserService, private router: Router, private pluginService: PluginService) {} async ngOnInit(): Promise { const plugins = await this.pluginService.getPlugins().toPromise(); const routes = this.buildRoutes(plugins); this.router.resetConfig(routes); + await this.userService.login(); } buildRoutes(plugins: PluginDefinition[]): Routes { diff --git a/webapp/src/app/pages/expedition/expedition-routing.module.ts b/webapp/src/app/pages/expedition/expedition-routing.module.ts index f544093..3c779ed 100644 --- a/webapp/src/app/pages/expedition/expedition-routing.module.ts +++ b/webapp/src/app/pages/expedition/expedition-routing.module.ts @@ -1,11 +1,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { ExpeditionComponent } from './routes/root/expedition.component'; +import { LoginGuard } from '../../guards/login.guard'; const routes: Routes = [ { path: '', - component: ExpeditionComponent + component: ExpeditionComponent, + canActivate: [LoginGuard] } ]; diff --git a/webapp/src/app/services/user/user.service.ts b/webapp/src/app/services/user/user.service.ts index 516c855..4d86ffa 100644 --- a/webapp/src/app/services/user/user.service.ts +++ b/webapp/src/app/services/user/user.service.ts @@ -30,7 +30,6 @@ export class UserService { this.router.navigate(['account-disabled']); return; } - this.router.navigate(['']); } else { if (response.newUser) { this.router.navigate(['register']); From 4fe614a4706e144e3e5a04a581b8d81b19b8e461 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Sun, 28 Nov 2021 19:17:53 +0100 Subject: [PATCH 09/25] fix: fixed column size in company character table --- .../characters-table/characters-table.component.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/webapp/src/app/pages/company/components/characters-table/characters-table.component.css b/webapp/src/app/pages/company/components/characters-table/characters-table.component.css index 2f95087..633ff4e 100644 --- a/webapp/src/app/pages/company/components/characters-table/characters-table.component.css +++ b/webapp/src/app/pages/company/components/characters-table/characters-table.component.css @@ -8,8 +8,8 @@ .mat-cell { word-wrap: break-word !important; white-space: unset !important; - flex: 0 0 120px !important; - width: 120px !important; + flex: 0 0 100px !important; + width: 100px !important; overflow-wrap: break-word; word-break: break-word; @@ -24,8 +24,8 @@ } .mat-header-cell { - flex: 0 0 120px !important; - width: 120px !important; + flex: 0 0 100px !important; + width: 100px !important; } .mat-row:hover .mat-cell { @@ -35,8 +35,8 @@ .mat-column-characterName { word-wrap: break-word !important; white-space: unset !important; - flex: 0 0 240px !important; - width: 240px !important; + flex: 0 0 180px !important; + width: 180px !important; overflow-wrap: break-word; word-break: break-word; From 5303348f0b10e676859b503dd3e600f3433d628a Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Sun, 28 Nov 2021 20:15:08 +0100 Subject: [PATCH 10/25] feat: delete user --- libs/model/src/admin.model.ts | 4 +++ server/src/admin/admin.controller.ts | 8 ++++- server/src/admin/dto/user.delete.dto.ts | 7 ++++ server/src/user/user.service.ts | 8 +++++ .../src/app/pages/admin/admin-page.module.ts | 11 +++++-- .../users-table.component.html | 11 +++++++ .../users-table.component.ts | 32 +++++++++++++++++-- .../character-delete.component.css | 0 .../character-delete.component.html | 6 ++++ .../character-delete.component.spec.ts | 25 +++++++++++++++ .../character-delete.component.ts | 25 +++++++++++++++ .../src/app/services/admin/admin.service.ts | 9 ++++-- .../app/services/snackbar/snackbar.service.ts | 4 +-- 13 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 server/src/admin/dto/user.delete.dto.ts create mode 100644 webapp/src/app/pages/admin/components/character-delete/character-delete.component.css create mode 100644 webapp/src/app/pages/admin/components/character-delete/character-delete.component.html create mode 100644 webapp/src/app/pages/admin/components/character-delete/character-delete.component.spec.ts create mode 100644 webapp/src/app/pages/admin/components/character-delete/character-delete.component.ts diff --git a/libs/model/src/admin.model.ts b/libs/model/src/admin.model.ts index 88fb823..a06d604 100644 --- a/libs/model/src/admin.model.ts +++ b/libs/model/src/admin.model.ts @@ -11,3 +11,7 @@ export interface EnableUser { userId: number; enabled: boolean; } + +export interface DeleteUser { + userId: number; +} diff --git a/server/src/admin/admin.controller.ts b/server/src/admin/admin.controller.ts index a093407..35aebd4 100644 --- a/server/src/admin/admin.controller.ts +++ b/server/src/admin/admin.controller.ts @@ -1,10 +1,11 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, Post } from '@nestjs/common'; import { UserService } from '../user/user.service'; import { AdminService } from './admin.service'; import { Public, RequiredPermissions } from '../login/login.decorator'; import { EnableUserDto } from './dto/user.enable.dto'; import { AdminUserDto } from './dto/user.admin.dto'; import { Permission, UserWithPermissions, Version } from '@nw-company-tool/model'; +import { DeleteUserDto } from './dto/user.delete.dto'; @Controller('/api/admin') @RequiredPermissions(Permission.ADMIN) @@ -16,6 +17,11 @@ export class AdminController { return this.userService.findAllWithPermissions(); } + @Post('/users/delete') + async deleteUser(@Body() body: DeleteUserDto): Promise { + return this.userService.delete(body.userId); + } + @Post('/users/enable') async setEnabled(@Body() body: EnableUserDto): Promise { if (body.enabled) { diff --git a/server/src/admin/dto/user.delete.dto.ts b/server/src/admin/dto/user.delete.dto.ts new file mode 100644 index 0000000..e14a339 --- /dev/null +++ b/server/src/admin/dto/user.delete.dto.ts @@ -0,0 +1,7 @@ +import { DeleteUser } from '@nw-company-tool/model'; +import { IsNumber } from 'class-validator'; + +export class DeleteUserDto implements DeleteUser { + @IsNumber() + userId: number; +} diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index a4bac68..13112fc 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -76,6 +76,14 @@ export class UserService { } } + async delete(id: number): Promise { + await this.client.user.delete({ + where: { + id, + }, + }); + } + async setCharacterName(id: number, characterName: string): Promise { return this.client.user.update({ where: { id }, data: { characterName } }); } diff --git a/webapp/src/app/pages/admin/admin-page.module.ts b/webapp/src/app/pages/admin/admin-page.module.ts index bc751a0..44b9bcc 100644 --- a/webapp/src/app/pages/admin/admin-page.module.ts +++ b/webapp/src/app/pages/admin/admin-page.module.ts @@ -11,6 +11,10 @@ import { MatInputModule } from '@angular/material/input'; import { MatCardModule } from '@angular/material/card'; import { MatTabsModule } from '@angular/material/tabs'; import { CommonModule } from '@angular/common'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { CharacterDeleteComponent } from './components/character-delete/character-delete.component'; +import { MatDialogModule } from '@angular/material/dialog'; @NgModule({ imports: [ @@ -22,9 +26,12 @@ import { CommonModule } from '@angular/common'; MatInputModule, MatCardModule, MatTabsModule, - CommonModule + CommonModule, + MatIconModule, + MatTooltipModule, + MatDialogModule ], - declarations: [AdminComponent, UsersTableComponent, UpdateComponent], + declarations: [AdminComponent, UsersTableComponent, UpdateComponent, CharacterDeleteComponent], providers: [], exports: [] }) diff --git a/webapp/src/app/pages/admin/components/admin-users-table/users-table.component.html b/webapp/src/app/pages/admin/components/admin-users-table/users-table.component.html index f179a1d..c8cfa17 100644 --- a/webapp/src/app/pages/admin/components/admin-users-table/users-table.component.html +++ b/webapp/src/app/pages/admin/components/admin-users-table/users-table.component.html @@ -46,6 +46,17 @@ + + + Delete + + + + + diff --git a/webapp/src/app/pages/admin/components/admin-users-table/users-table.component.ts b/webapp/src/app/pages/admin/components/admin-users-table/users-table.component.ts index 8882c45..c7e6f76 100644 --- a/webapp/src/app/pages/admin/components/admin-users-table/users-table.component.ts +++ b/webapp/src/app/pages/admin/components/admin-users-table/users-table.component.ts @@ -6,6 +6,8 @@ import { UserService } from '../../../../services/user/user.service'; import { SnackbarService } from '../../../../services/snackbar/snackbar.service'; import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; +import { MatDialog } from '@angular/material/dialog'; +import { CharacterDeleteComponent } from '../character-delete/character-delete.component'; @Component({ selector: 'app-users-table', @@ -13,7 +15,7 @@ import { Observable } from 'rxjs'; styleUrls: ['./users-table.component.css'] }) export class UsersTableComponent implements OnInit { - displayedColumns: string[] = ['id', 'characterName', 'discordUsername', 'discordId', 'enabled', 'admin']; + displayedColumns: string[] = ['id', 'characterName', 'discordUsername', 'discordId', 'enabled', 'admin', 'delete']; displayedData: UserWithPermissions[] = []; data: UserWithPermissions[] = []; @@ -21,7 +23,8 @@ export class UsersTableComponent implements OnInit { constructor( private adminService: AdminService, private userService: UserService, - private snackbarService: SnackbarService + private snackbarService: SnackbarService, + private dialog: MatDialog ) {} ngOnInit(): void { @@ -81,4 +84,29 @@ export class UsersTableComponent implements OnInit { isAdmin(user: UserWithPermissions): boolean { return !!user?.permissions?.includes(Permission.ADMIN); } + + async delete(user: UserWithPermissions): Promise { + await this.userService + .getUser$() + .pipe( + map((currentUser) => { + //dont delete yourself ;) + if (currentUser.id === user.id) { + return; + } + const dialogRef = this.dialog.open(CharacterDeleteComponent, { + data: user + }); + dialogRef.afterClosed().subscribe((result) => { + if (result === 'delete') { + this.adminService.delete(user.id).subscribe(() => { + this.snackbarService.open(`User deleted: ${user.characterName}`); + this.displayedData = this.displayedData.filter((it) => it.id != user.id); + }); + } + }); + }) + ) + .toPromise(); + } } diff --git a/webapp/src/app/pages/admin/components/character-delete/character-delete.component.css b/webapp/src/app/pages/admin/components/character-delete/character-delete.component.css new file mode 100644 index 0000000..e69de29 diff --git a/webapp/src/app/pages/admin/components/character-delete/character-delete.component.html b/webapp/src/app/pages/admin/components/character-delete/character-delete.component.html new file mode 100644 index 0000000..8e2de81 --- /dev/null +++ b/webapp/src/app/pages/admin/components/character-delete/character-delete.component.html @@ -0,0 +1,6 @@ +

Delete {{user.characterName}}

+
Do you really want to delete {{user.characterName}}
+
+ + +
diff --git a/webapp/src/app/pages/admin/components/character-delete/character-delete.component.spec.ts b/webapp/src/app/pages/admin/components/character-delete/character-delete.component.spec.ts new file mode 100644 index 0000000..004fa53 --- /dev/null +++ b/webapp/src/app/pages/admin/components/character-delete/character-delete.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CharacterDeleteComponent } from './character-delete.component'; + +describe('CharacterDeleteComponent', () => { + let component: CharacterDeleteComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CharacterDeleteComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CharacterDeleteComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/webapp/src/app/pages/admin/components/character-delete/character-delete.component.ts b/webapp/src/app/pages/admin/components/character-delete/character-delete.component.ts new file mode 100644 index 0000000..a2cf8e2 --- /dev/null +++ b/webapp/src/app/pages/admin/components/character-delete/character-delete.component.ts @@ -0,0 +1,25 @@ +import { Component, Inject } from '@angular/core'; +import { AdminService } from '../../../../services/admin/admin.service'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { UserWithPermissions } from '@nw-company-tool/model'; + +@Component({ + selector: 'app-character-delete', + templateUrl: './character-delete.component.html', + styleUrls: ['./character-delete.component.css'] +}) +export class CharacterDeleteComponent { + constructor( + private adminService: AdminService, + @Inject(MAT_DIALOG_DATA) public user: UserWithPermissions, + private dialogRef: MatDialogRef + ) {} + + abort() { + this.dialogRef.close(); + } + + delete() { + this.dialogRef.close('delete'); + } +} diff --git a/webapp/src/app/services/admin/admin.service.ts b/webapp/src/app/services/admin/admin.service.ts index 8edd0e9..da454d7 100644 --- a/webapp/src/app/services/admin/admin.service.ts +++ b/webapp/src/app/services/admin/admin.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map, mergeMap } from 'rxjs/operators'; -import { AdminUser, EnableUser, UserWithPermissions, Version } from '@nw-company-tool/model'; +import { AdminUser, DeleteUser, EnableUser, UserWithPermissions, Version } from '@nw-company-tool/model'; @Injectable({ providedIn: 'root' @@ -48,6 +48,11 @@ export class AdminService { } public update(): Observable { - return this.http.post('api/admin/server/update', {}, { withCredentials: true }); + return this.http.post('/api/admin/server/update', {}, { withCredentials: true }); + } + + public delete(id: number): Observable { + const payload: DeleteUser = { userId: id }; + return this.http.post('/api/admin/users/delete', payload, { withCredentials: true }); } } diff --git a/webapp/src/app/services/snackbar/snackbar.service.ts b/webapp/src/app/services/snackbar/snackbar.service.ts index 2d8c495..5e8371b 100644 --- a/webapp/src/app/services/snackbar/snackbar.service.ts +++ b/webapp/src/app/services/snackbar/snackbar.service.ts @@ -10,8 +10,8 @@ export class SnackbarService { open(message: string, duration = 1500): void { this.snackBar.open(message, 'OK', { duration, - horizontalPosition: 'start', - verticalPosition: 'bottom', + horizontalPosition: 'center', + verticalPosition: 'top', panelClass: ['snackbar-info'] }); } From 85b3a730030a1c904e32a4ac90132e9373f4bd5e Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Tue, 30 Nov 2021 18:51:11 +0100 Subject: [PATCH 11/25] feat: expedition planning --- libs/model/src/calendar.model.ts | 4 +- libs/model/src/expedition.model.ts | 41 ++++- .../20211130174250_expedition/migration.sql | 23 +++ server/schema.prisma | 34 +++- server/src/app.module.ts | 2 + .../expedition/dto/expedition.create.dto.ts | 16 ++ .../expedition/dto/expedition.delete.dto.ts | 7 + .../src/expedition/dto/expedition.join.dto.ts | 11 ++ .../expedition/dto/expedition.leave.dto.ts | 7 + .../src/expedition/expedition.controller.ts | 45 +++++ server/src/expedition/expedition.module.ts | 12 ++ server/src/expedition/expedition.service.ts | 163 +++++++++++++++++ server/src/login/login.decorator.ts | 3 + server/src/login/login.guard.ts | 14 +- server/src/user/user.controller.ts | 3 +- webapp/src/app/app.component.ts | 1 - webapp/src/app/app.module.ts | 9 +- .../character-detail.component.css | 0 .../character-detail.component.html | 0 .../character-detail.component.spec.ts | 0 .../character-detail.component.ts | 2 +- webapp/src/app/guards/login.guard.ts | 29 +-- .../src/app/interceptor/token.interceptor.ts | 1 + .../app/pages/company/company-page.module.ts | 3 +- .../characters-table.component.ts | 2 +- .../expedition-create.component.html | 13 +- .../expedition-create.component.ts | 56 +++--- .../expedition-join.component.css | 0 .../expedition-join.component.html | 21 +++ .../expedition-join.component.spec.ts | 25 +++ .../expedition-join.component.ts | 55 ++++++ .../expedition-table.component.css | 51 ++++++ .../expedition-table.component.html | 85 ++++++--- .../expedition-table.component.ts | 169 +++++++++++++++++- .../expedition/expedition-page.module.ts | 11 +- .../routes/root/expedition.component.html | 3 - .../home-calendar/home-calendar.component.css | 16 ++ .../home-calendar/home-calendar.component.ts | 83 ++++++--- .../services/expedition/expedition.service.ts | 51 +++++- webapp/src/app/services/user/user.service.ts | 49 +++-- webapp/src/assets/i18n/en.json | 8 + 41 files changed, 985 insertions(+), 143 deletions(-) create mode 100644 server/migrations/20211130174250_expedition/migration.sql create mode 100644 server/src/expedition/dto/expedition.create.dto.ts create mode 100644 server/src/expedition/dto/expedition.delete.dto.ts create mode 100644 server/src/expedition/dto/expedition.join.dto.ts create mode 100644 server/src/expedition/dto/expedition.leave.dto.ts create mode 100644 server/src/expedition/expedition.controller.ts create mode 100644 server/src/expedition/expedition.module.ts create mode 100644 server/src/expedition/expedition.service.ts rename webapp/src/app/{pages/company => }/components/character-detail/character-detail.component.css (100%) rename webapp/src/app/{pages/company => }/components/character-detail/character-detail.component.html (100%) rename webapp/src/app/{pages/company => }/components/character-detail/character-detail.component.spec.ts (100%) rename webapp/src/app/{pages/company => }/components/character-detail/character-detail.component.ts (95%) create mode 100644 webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.css create mode 100644 webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.html create mode 100644 webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.spec.ts create mode 100644 webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.ts diff --git a/libs/model/src/calendar.model.ts b/libs/model/src/calendar.model.ts index 9ade4cd..14272a3 100644 --- a/libs/model/src/calendar.model.ts +++ b/libs/model/src/calendar.model.ts @@ -1 +1,3 @@ -export type CalendarEvent = {}; +export enum CalendarEventType { + EXPEDITION = 'EXPEDITION', +} diff --git a/libs/model/src/expedition.model.ts b/libs/model/src/expedition.model.ts index 08fec85..58e902e 100644 --- a/libs/model/src/expedition.model.ts +++ b/libs/model/src/expedition.model.ts @@ -1,4 +1,26 @@ +export type CreateExpedition = { + name: ExpeditionName; + beginDateTime: string; + hasTuningOrb: boolean; + role: Role; +}; + +export type DeleteExpedition = { + id: number; +}; + +export type JoinExpedition = { + id: number; + role: Role; + hasTuningOrb: boolean; +}; + +export type LeaveExpedition = { + id: number; +}; + export type Expedition = { + id: number; name: string; beginDateTime: string; participants: Participant[]; @@ -9,8 +31,8 @@ export type Participant = { userId: number; characterName: string; discordId: string; - role: 'tank' | 'damage' | 'heal'; - hasKey: boolean; + role: Role; + hasTuningOrb: boolean; }; export type Owner = { @@ -18,3 +40,18 @@ export type Owner = { characterName: string; discordId: string; }; + +export enum Role { + TANK = 'TANK', + DAMAGE = 'DAMAGE', + HEAL = 'HEAL', +} + +export enum ExpeditionName { + AMRINE_EXCAVATION = 'AMRINE_EXCAVATION', + STARSTONE_BARROWS = 'STARSTONE_BARROWS', + THE_DEPTHS = 'THE_DEPTHS', + DYNASTY_SHIPYARD = 'DYNASTY_SHIPYARD', + LAZARUS_INSTRUMENTALITY = 'LAZARUS_INSTRUMENTALITY', + GARDEN_OF_GENESIS = 'GARDEN_OF_GENESIS', +} diff --git a/server/migrations/20211130174250_expedition/migration.sql b/server/migrations/20211130174250_expedition/migration.sql new file mode 100644 index 0000000..62b58ec --- /dev/null +++ b/server/migrations/20211130174250_expedition/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "Expedition" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "userId" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "beginDateTime" DATETIME NOT NULL, + CONSTRAINT "Expedition_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ExpeditionParticipant" ( + "expeditionId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "hasTuningOrb" BOOLEAN NOT NULL DEFAULT false, + "role" TEXT NOT NULL, + + PRIMARY KEY ("expeditionId", "userId"), + CONSTRAINT "ExpeditionParticipant_expeditionId_fkey" FOREIGN KEY ("expeditionId") REFERENCES "Expedition" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ExpeditionParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Expedition_id_uindex" ON "Expedition"("id"); diff --git a/server/schema.prisma b/server/schema.prisma index 026e2d0..c7635bb 100644 --- a/server/schema.prisma +++ b/server/schema.prisma @@ -17,12 +17,14 @@ model CharacterAttribute { } model User { - id Int @id @unique(map: "User_id_uindex") @default(autoincrement()) - discordId String @unique(map: "User_discordId_uindex") - discordUsername String - characterName String - CharacterAttribute CharacterAttribute[] - UserPermission UserPermission[] + id Int @id @unique(map: "User_id_uindex") @default(autoincrement()) + discordId String @unique(map: "User_discordId_uindex") + discordUsername String + characterName String + CharacterAttribute CharacterAttribute[] + Expedition Expedition[] + ExpeditionParticipant ExpeditionParticipant[] + UserPermission UserPermission[] } model UserPermission { @@ -32,3 +34,23 @@ model UserPermission { @@id([userId, permission]) } + +model Expedition { + id Int @id @unique(map: "Expedition_id_uindex") @default(autoincrement()) + userId Int + name String + beginDateTime DateTime + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + ExpeditionParticipant ExpeditionParticipant[] +} + +model ExpeditionParticipant { + expeditionId Int + userId Int + hasTuningOrb Boolean @default(false) + role String + Expedition Expedition @relation(fields: [expeditionId], references: [id], onDelete: Cascade) + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@id([expeditionId, userId]) +} diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 543e749..aedce38 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -9,6 +9,7 @@ import { AdminModule } from './admin/admin.module'; import { CharacterModule } from './character/character.module'; import { PluginModule } from './plugin/plugin.module'; import { FrontendMiddleware } from './middleware/frontend.middleware'; +import { ExpeditionModule } from './expedition/expedition.module'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { FrontendMiddleware } from './middleware/frontend.middleware'; ConfigModule, ArgsModule, DatabaseModule, + ExpeditionModule, UserModule, LoginModule, AdminModule, diff --git a/server/src/expedition/dto/expedition.create.dto.ts b/server/src/expedition/dto/expedition.create.dto.ts new file mode 100644 index 0000000..e3ac487 --- /dev/null +++ b/server/src/expedition/dto/expedition.create.dto.ts @@ -0,0 +1,16 @@ +import { CreateExpedition, ExpeditionName, Role } from '@nw-company-tool/model'; +import { IsBoolean, IsDateString, IsEnum } from 'class-validator'; + +export class CreateExpeditionDto implements CreateExpedition { + @IsDateString() + beginDateTime: string; + + @IsBoolean() + hasTuningOrb: boolean; + + @IsEnum(ExpeditionName) + name: ExpeditionName; + + @IsEnum(Role) + role: Role; +} diff --git a/server/src/expedition/dto/expedition.delete.dto.ts b/server/src/expedition/dto/expedition.delete.dto.ts new file mode 100644 index 0000000..3217cba --- /dev/null +++ b/server/src/expedition/dto/expedition.delete.dto.ts @@ -0,0 +1,7 @@ +import { DeleteExpedition } from '@nw-company-tool/model'; +import { IsNumber } from 'class-validator'; + +export class DeleteExpeditionDto implements DeleteExpedition { + @IsNumber() + id: number; +} diff --git a/server/src/expedition/dto/expedition.join.dto.ts b/server/src/expedition/dto/expedition.join.dto.ts new file mode 100644 index 0000000..fc6935b --- /dev/null +++ b/server/src/expedition/dto/expedition.join.dto.ts @@ -0,0 +1,11 @@ +import { JoinExpedition, Role } from '@nw-company-tool/model'; +import { IsBoolean, IsEnum, IsNumber } from 'class-validator'; + +export class JoinExpeditionDto implements JoinExpedition { + @IsBoolean() + hasTuningOrb: boolean; + @IsNumber() + id: number; + @IsEnum(Role) + role: Role; +} diff --git a/server/src/expedition/dto/expedition.leave.dto.ts b/server/src/expedition/dto/expedition.leave.dto.ts new file mode 100644 index 0000000..f00fe02 --- /dev/null +++ b/server/src/expedition/dto/expedition.leave.dto.ts @@ -0,0 +1,7 @@ +import { LeaveExpedition } from '@nw-company-tool/model'; +import { IsNumber } from 'class-validator'; + +export class LeaveExpeditionDto implements LeaveExpedition { + @IsNumber() + id: number; +} diff --git a/server/src/expedition/expedition.controller.ts b/server/src/expedition/expedition.controller.ts new file mode 100644 index 0000000..9145f61 --- /dev/null +++ b/server/src/expedition/expedition.controller.ts @@ -0,0 +1,45 @@ +import { Body, Controller, Get, HttpException, Post, Req } from '@nestjs/common'; +import { RequiredPermissions } from '../login/login.decorator'; +import { Expedition, Permission } from '@nw-company-tool/model'; +import { CreateExpeditionDto } from './dto/expedition.create.dto'; +import { Request } from '../app.model'; +import { ExpeditionService } from './expedition.service'; +import { DeleteExpeditionDto } from './dto/expedition.delete.dto'; +import { JoinExpeditionDto } from './dto/expedition.join.dto'; +import { LeaveExpeditionDto } from './dto/expedition.leave.dto'; + +@Controller('/api/expedition') +@RequiredPermissions(Permission.ENABLED) +export class ExpeditionController { + constructor(private expeditionService: ExpeditionService) {} + + @Get('/') + getAllExpeditions(): Promise { + return this.expeditionService.findAll(); + } + + @Post('/') + async createExpedition(@Req() request: Request, @Body() body: CreateExpeditionDto): Promise { + await this.expeditionService.create(request.user, body); + } + + @Post('/delete') + async deleteExpedition(@Req() request: Request, @Body() body: DeleteExpeditionDto): Promise { + const expedition = await this.expeditionService.findById(body.id); + if (request.user.id === expedition.owner.userId || request.user.permissions.includes(Permission.ADMIN)) { + await this.expeditionService.delete(body.id); + } else { + throw new HttpException('you are not the owner of this expedition', 403); + } + } + + @Post('/join') + async joinExpedition(@Req() request: Request, @Body() body: JoinExpeditionDto): Promise { + await this.expeditionService.join(request.user, body); + } + + @Post('/leave') + async leaveExpedition(@Req() request: Request, @Body() body: LeaveExpeditionDto): Promise { + await this.expeditionService.leave(request.user, body); + } +} diff --git a/server/src/expedition/expedition.module.ts b/server/src/expedition/expedition.module.ts new file mode 100644 index 0000000..aea797a --- /dev/null +++ b/server/src/expedition/expedition.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { DatabaseModule } from '../database/database.module'; +import { ExpeditionController } from './expedition.controller'; +import { ExpeditionService } from './expedition.service'; + +@Module({ + imports: [DatabaseModule], + controllers: [ExpeditionController], + providers: [ExpeditionService], + exports: [], +}) +export class ExpeditionModule {} diff --git a/server/src/expedition/expedition.service.ts b/server/src/expedition/expedition.service.ts new file mode 100644 index 0000000..846569a --- /dev/null +++ b/server/src/expedition/expedition.service.ts @@ -0,0 +1,163 @@ +import { HttpException, Injectable } from '@nestjs/common'; +import { + CreateExpedition, + Expedition, + JoinExpedition, + LeaveExpedition, + Participant, + Role, + User, +} from '@nw-company-tool/model'; +import { ExpeditionParticipant } from '@prisma/client'; +import { DatabaseClient } from '../database/database.client'; + +type ExpeditionQueryResult = { + id: number; + name: string; + beginDateTime: Date; + User: User; + ExpeditionParticipant: (ExpeditionParticipant & { User: User })[]; +}; + +@Injectable() +export class ExpeditionService { + constructor(private client: DatabaseClient) {} + + findAll(): Promise { + return this.client.expedition + .findMany({ + select: { + id: true, + name: true, + beginDateTime: true, + User: true, + ExpeditionParticipant: { + include: { + User: true, + }, + }, + }, + }) + .then((results) => results.map((result) => this.mapToExpedition(result))); + } + + async findById(id: number): Promise { + return this.client.expedition + .findUnique({ + where: { + id, + }, + select: { + id: true, + name: true, + beginDateTime: true, + User: true, + ExpeditionParticipant: { + include: { + User: true, + }, + }, + }, + }) + .then((result) => this.mapToExpedition(result)); + } + + async findParticipants(id: number): Promise { + return this.client.expeditionParticipant + .findMany({ + include: { + User: true, + }, + where: { + expeditionId: id, + }, + }) + .then((participants) => participants.map((participant) => ExpeditionService.mapToParticipant(participant))); + } + + async join(user: User, expeditionJoin: JoinExpedition): Promise { + const participants = await this.findParticipants(expeditionJoin.id); + const roles = participants.filter((participant) => participant.role === expeditionJoin.role); + switch (expeditionJoin.role) { + case Role.DAMAGE: { + if (roles.length >= 3) throw new HttpException('expedition is full', 400); + } + case Role.TANK: + case Role.HEAL: { + if (roles.length >= 1) throw new HttpException('expedition is full', 400); + } + } + await this.client.expeditionParticipant.create({ + data: { + userId: user.id, + expeditionId: expeditionJoin.id, + hasTuningOrb: expeditionJoin.hasTuningOrb, + role: expeditionJoin.role, + }, + }); + } + + async leave(user: User, expeditionLeave: LeaveExpedition): Promise { + await this.client.expeditionParticipant.delete({ + where: { + expeditionId_userId: { + userId: user.id, + expeditionId: expeditionLeave.id, + }, + }, + }); + } + + async create(user: User, expeditionData: CreateExpedition): Promise { + await this.client.expedition.create({ + data: { + name: expeditionData.name, + beginDateTime: expeditionData.beginDateTime, + User: { + connect: { + id: user.id, + }, + }, + ExpeditionParticipant: { + create: { + userId: user.id, + hasTuningOrb: expeditionData.hasTuningOrb, + role: expeditionData.role, + }, + }, + }, + }); + } + + async delete(id: number): Promise { + await this.client.expedition.delete({ + where: { + id, + }, + }); + } + + private mapToExpedition(result: ExpeditionQueryResult): Expedition { + return { + id: result.id, + name: result.name, + beginDateTime: result.beginDateTime.toISOString(), + owner: { + userId: result.User.id, + characterName: result.User.characterName, + discordId: result.User.discordId, + }, + participants: result.ExpeditionParticipant.map((participant) => ExpeditionService.mapToParticipant(participant)), + }; + } + + private static mapToParticipant(participant: ExpeditionParticipant & { User: User }): Participant { + return { + userId: participant.User.id, + characterName: participant.User.characterName, + discordId: participant.User.discordId, + role: participant.role as Role, + hasTuningOrb: participant.hasTuningOrb, + }; + } +} diff --git a/server/src/login/login.decorator.ts b/server/src/login/login.decorator.ts index 800e440..e611886 100644 --- a/server/src/login/login.decorator.ts +++ b/server/src/login/login.decorator.ts @@ -3,5 +3,8 @@ import { SetMetadata } from '@nestjs/common'; export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); +export const IS_LOGGED_IN_KEY = 'isLoggedIn'; +export const LoggedIn = () => SetMetadata(IS_LOGGED_IN_KEY, true); + export const ROLES_KEY = 'roles'; export const RequiredPermissions = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/server/src/login/login.guard.ts b/server/src/login/login.guard.ts index 07c2726..6076833 100644 --- a/server/src/login/login.guard.ts +++ b/server/src/login/login.guard.ts @@ -1,5 +1,5 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; -import { IS_PUBLIC_KEY, ROLES_KEY } from './login.decorator'; +import { IS_LOGGED_IN_KEY, IS_PUBLIC_KEY, ROLES_KEY } from './login.decorator'; import { Reflector } from '@nestjs/core'; import { Cookies, Request } from '../app.model'; import { UserService } from '../user/user.service'; @@ -18,6 +18,12 @@ export class LoginGuard implements CanActivate { if (isPublic) { return true; } + + const isLoggedIn = this.reflector.getAllAndOverride(IS_LOGGED_IN_KEY, [ + context.getHandler(), + context.getClass(), + ]); + const request = context.switchToHttp().getRequest(); const cookies: Cookies = request.cookies; const accessToken = this.tokenService.parseAccessToken(cookies.access_token); @@ -25,6 +31,12 @@ export class LoginGuard implements CanActivate { request.user = user; // TODO do this in middleware!! request.discordAvatar = accessToken.discordAvatar; + if (isLoggedIn) { + if (user) { + return true; + } + } + const requiredPermissions = this.reflector.getAllAndOverride(ROLES_KEY, [ context.getHandler(), context.getClass(), diff --git a/server/src/user/user.controller.ts b/server/src/user/user.controller.ts index b85c732..a6fe30e 100644 --- a/server/src/user/user.controller.ts +++ b/server/src/user/user.controller.ts @@ -3,7 +3,7 @@ import { Cookies, Request } from '../app.model'; import { UserService } from './user.service'; import { TokenService } from '../token/token.service'; import { SetCharacterNameDto } from './dto/charactername.set.dto'; -import { Public, RequiredPermissions } from '../login/login.decorator'; +import { LoggedIn, Public, RequiredPermissions } from '../login/login.decorator'; import { Permission, UserAvatar, UserWithPermissions } from '@nw-company-tool/model'; @Controller('/api/user') @@ -24,6 +24,7 @@ export class UserController { await this.userService.setCharacterName(request.user.id, body.characterName); } + @LoggedIn() @Get('/avatar') getAvatar(@Req() request: Request, @Query('size') size: number): UserAvatar { return this.userService.getAvatar(request.user, request.discordAvatar, size); diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts index 9c3dd29..27e0a2d 100644 --- a/webapp/src/app/app.component.ts +++ b/webapp/src/app/app.component.ts @@ -18,7 +18,6 @@ export class AppComponent implements OnInit { const plugins = await this.pluginService.getPlugins().toPromise(); const routes = this.buildRoutes(plugins); this.router.resetConfig(routes); - await this.userService.login(); } buildRoutes(plugins: PluginDefinition[]): Routes { diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts index 64681da..4c06f1d 100644 --- a/webapp/src/app/app.module.ts +++ b/webapp/src/app/app.module.ts @@ -32,6 +32,9 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; import interactionPlugin from '@fullcalendar/interaction'; import { ExpeditionModule } from './services/expedition/expedition.module'; +import { CharacterDetailComponent } from './components/character-detail/character-detail.component'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatDialogModule } from '@angular/material/dialog'; const cookieConfig: NgcCookieConsentConfig = { cookie: { @@ -65,7 +68,7 @@ const i18nConfig: TranslateModuleConfig = { FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPlugin]); @NgModule({ - declarations: [AppComponent, HeaderComponent, FooterComponent], + declarations: [AppComponent, HeaderComponent, FooterComponent, CharacterDetailComponent], imports: [ NgcCookieConsentModule.forRoot(cookieConfig), TranslateModule.forRoot(i18nConfig), @@ -90,7 +93,9 @@ FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPl MatMenuModule, MatIconModule, MatButtonModule, - MatMomentDateModule + MatMomentDateModule, + MatProgressBarModule, + MatDialogModule ], providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }, AppComponent, LoginGuard, AdminGuard], bootstrap: [AppComponent], diff --git a/webapp/src/app/pages/company/components/character-detail/character-detail.component.css b/webapp/src/app/components/character-detail/character-detail.component.css similarity index 100% rename from webapp/src/app/pages/company/components/character-detail/character-detail.component.css rename to webapp/src/app/components/character-detail/character-detail.component.css diff --git a/webapp/src/app/pages/company/components/character-detail/character-detail.component.html b/webapp/src/app/components/character-detail/character-detail.component.html similarity index 100% rename from webapp/src/app/pages/company/components/character-detail/character-detail.component.html rename to webapp/src/app/components/character-detail/character-detail.component.html diff --git a/webapp/src/app/pages/company/components/character-detail/character-detail.component.spec.ts b/webapp/src/app/components/character-detail/character-detail.component.spec.ts similarity index 100% rename from webapp/src/app/pages/company/components/character-detail/character-detail.component.spec.ts rename to webapp/src/app/components/character-detail/character-detail.component.spec.ts diff --git a/webapp/src/app/pages/company/components/character-detail/character-detail.component.ts b/webapp/src/app/components/character-detail/character-detail.component.ts similarity index 95% rename from webapp/src/app/pages/company/components/character-detail/character-detail.component.ts rename to webapp/src/app/components/character-detail/character-detail.component.ts index 5a16399..48c63ba 100644 --- a/webapp/src/app/pages/company/components/character-detail/character-detail.component.ts +++ b/webapp/src/app/components/character-detail/character-detail.component.ts @@ -1,7 +1,7 @@ import { Component, Inject, OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Attribute, Character } from '@nw-company-tool/model'; -import { CharacterService } from '../../../../services/character/character.service'; +import { CharacterService } from '../../services/character/character.service'; export interface CharacterDetailDialogData { id: number; diff --git a/webapp/src/app/guards/login.guard.ts b/webapp/src/app/guards/login.guard.ts index 5c85130..af658fb 100644 --- a/webapp/src/app/guards/login.guard.ts +++ b/webapp/src/app/guards/login.guard.ts @@ -1,22 +1,31 @@ -import { CanActivate } from '@angular/router'; +import { CanActivate, Router } from '@angular/router'; import { Injectable } from '@angular/core'; import { UserService } from '../services/user/user.service'; -import { Observable, of } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { map, mergeMap } from 'rxjs/operators'; import { Permission } from '@nw-company-tool/model'; @Injectable() export class LoginGuard implements CanActivate { - constructor(private userService: UserService) {} + constructor(private userService: UserService, private router: Router) {} canActivate(): Observable { - if (!this.userService.isLoggedIn()) { - this.userService.login(); - return of(false); - } - return this.userService.getUser$().pipe( + return this.userService.isLoggedIn$().pipe( + mergeMap((isLoggedIn) => { + if (isLoggedIn) { + return this.userService.getUser$(); + } + return this.userService.login(); + }), map((user) => { - return user.permissions.includes(Permission.ENABLED); + if (!user) { + return false; + } + if (!user.permissions.includes(Permission.ENABLED)) { + this.router.navigate(['account-disabled']); + return false; + } + return true; }) ); } diff --git a/webapp/src/app/interceptor/token.interceptor.ts b/webapp/src/app/interceptor/token.interceptor.ts index 4636e56..d23650f 100644 --- a/webapp/src/app/interceptor/token.interceptor.ts +++ b/webapp/src/app/interceptor/token.interceptor.ts @@ -22,6 +22,7 @@ export class TokenInterceptor implements HttpInterceptor { } if (err.status === 403) { this.router.navigate(['forbidden']); + this.userService.refreshUser(); } } throw throwError(err); diff --git a/webapp/src/app/pages/company/company-page.module.ts b/webapp/src/app/pages/company/company-page.module.ts index 57ed60f..f667a6f 100644 --- a/webapp/src/app/pages/company/company-page.module.ts +++ b/webapp/src/app/pages/company/company-page.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; import { CompanyRoutingModule } from './company-routing.module'; import { CompanyComponent } from './routes/root/company.component'; import { CharactersTableComponent } from './components/characters-table/characters-table.component'; -import { CharacterDetailComponent } from './components/character-detail/character-detail.component'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatButtonModule } from '@angular/material/button'; @@ -40,7 +39,7 @@ import { CommonModule } from '@angular/common'; MatCardModule, MatTabsModule ], - declarations: [CompanyComponent, CharactersTableComponent, CharacterDetailComponent], + declarations: [CompanyComponent, CharactersTableComponent], providers: [], exports: [] }) diff --git a/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts b/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts index 39b111b..1d43566 100644 --- a/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts +++ b/webapp/src/app/pages/company/components/characters-table/characters-table.component.ts @@ -18,7 +18,7 @@ import { WEAPON_SKILLS_TWO_HANDED } from '@nw-company-tool/model'; import { CharacterService } from '../../../../services/character/character.service'; -import { CharacterDetailComponent } from '../character-detail/character-detail.component'; +import { CharacterDetailComponent } from '../../../../components/character-detail/character-detail.component'; export type FilterModel = { attributes: Attribute[]; diff --git a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.html b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.html index 0fbefd1..68154ff 100644 --- a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.html +++ b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.html @@ -13,11 +13,18 @@ Expedition - Expedition 1 - Expedition 2 + {{('EXPEDITION.' + expedition) | translate}} - I have a tuning orb + + Role + + Tank + Damage + Heal + + + I have a tuning orb
diff --git a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts index 840a6e2..02b08bd 100644 --- a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts +++ b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts @@ -4,12 +4,14 @@ import * as moment from 'moment'; import { Moment } from 'moment'; import { ExpeditionService } from '../../../../services/expedition/expedition.service'; import { MatDialogRef } from '@angular/material/dialog'; +import { ExpeditionName, Role } from '@nw-company-tool/model'; type CreateExpeditionForm = { date: Moment; time: string; - expedition: string; + expedition: ExpeditionName; hasKey: boolean; + role: Role; }; @Component({ @@ -18,11 +20,13 @@ type CreateExpeditionForm = { styleUrls: ['./expedition-create.component.css'] }) export class ExpeditionCreateComponent { + role = Role; form = new FormGroup({ date: new FormControl(moment(), [Validators.required]), time: new FormControl(moment().format('HH:mm'), [Validators.required]), expedition: new FormControl(null, [Validators.required]), - hasKey: new FormControl(false) + hasKey: new FormControl(false), + role: new FormControl(null, [Validators.required]) }); constructor( @@ -32,33 +36,29 @@ export class ExpeditionCreateComponent { create() { this.form.markAllAsTouched(); - const expedition: CreateExpeditionForm = this.form.value; - if (this.form.valid) { - this.expeditionService.createExpedition({ - name: expedition.expedition, - beginDateTime: expedition.date - .set({ - hour: +expedition.time.split(':')[0], - minute: +expedition.time.split(':')[1] - }) - .format('YYYY-MM-DDTHH:mm'), - participants: [ - { - userId: 1, - characterName: 'Krise', - discordId: '1', - hasKey: true, - role: 'damage' - } - ], - owner: { - userId: 1, - characterName: 'Krise', - discordId: '1' - } - }); - this.dialogRef.close(); + const formData: CreateExpeditionForm = this.form.value; + const beginDateTime = formData.date + .set({ + hour: +formData.time.split(':')[0], + minute: +formData.time.split(':')[1] + }) + .toISOString(); + this.expeditionService + .createExpedition({ + name: formData.expedition, + beginDateTime, + hasTuningOrb: formData.hasKey, + role: formData.role + }) + .subscribe(() => { + this.expeditionService.refreshExpeditions(); + this.dialogRef.close(); + }); } } + + getExpeditions(): ExpeditionName[] { + return Object.values(ExpeditionName); + } } diff --git a/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.css b/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.css new file mode 100644 index 0000000..e69de29 diff --git a/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.html b/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.html new file mode 100644 index 0000000..61ebb69 --- /dev/null +++ b/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.html @@ -0,0 +1,21 @@ +
+

{{('EXPEDITION.' + dialogData.expedition.name) | translate}}

+

+ + Owner: {{dialogData.expedition.owner.characterName}}

+

{{formatDate(dialogData.expedition.beginDateTime)}}

+

+ I have a tuning orb +
+ +
+ + +
diff --git a/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.spec.ts b/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.spec.ts new file mode 100644 index 0000000..35ffe38 --- /dev/null +++ b/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExpeditionJoinComponent } from './expedition-join.component'; + +describe('ExpeditionJoinComponent', () => { + let component: ExpeditionJoinComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ExpeditionJoinComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ExpeditionJoinComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.ts b/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.ts new file mode 100644 index 0000000..7b078de --- /dev/null +++ b/webapp/src/app/pages/expedition/components/expedition-join/expedition-join.component.ts @@ -0,0 +1,55 @@ +import { Component, Inject } from '@angular/core'; +import { ExpeditionService } from '../../../../services/expedition/expedition.service'; +import { Expedition } from '@nw-company-tool/model'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormControl } from '@angular/forms'; +import { Slot } from '../expedition-table/expedition-table.component'; +import * as moment from 'moment'; + +export type ExpeditionJoinDialogData = { + slot: Slot; + expedition: Expedition; +}; + +@Component({ + selector: 'app-expedition-join', + templateUrl: './expedition-join.component.html', + styleUrls: ['./expedition-join.component.css'] +}) +export class ExpeditionJoinComponent { + hasKeyForm = new FormControl(false); + + constructor( + private expeditionService: ExpeditionService, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public dialogData: ExpeditionJoinDialogData + ) {} + + joinExpedition(): void { + this.expeditionService.joinExpedition({ + id: this.dialogData.expedition.id, + hasTuningOrb: this.hasKeyForm.value, + role: this.dialogData.slot.role + }); + this.close(); + } + + close(): void { + this.dialogRef.close(); + } + + formatDate(date: string): string { + return moment + .utc(date) + .local() + .format(`${moment.localeData().longDateFormat('LL')} ${moment.localeData().longDateFormat('LT')}`); + } + + openInDiscord(discordId: string): void { + window.open(`discord://discordapp.com/users/${discordId}`); + } + + openOwnerInDiscord() { + this.openInDiscord(this.dialogData.expedition.owner.discordId); + } +} diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.css b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.css index e69de29..1f2350d 100644 --- a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.css +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.css @@ -0,0 +1,51 @@ +.expedition-table { + width: 100%; + margin-top: 24px; + margin-bottom: 24px; +} + + +.mat-cell { + word-wrap: break-word !important; + white-space: unset !important; + flex: 0 0 150px !important; + width: 150px !important; + overflow-wrap: break-word; + + word-break: break-word; + + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; + + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; +} + +.mat-header-cell { + flex: 0 0 150px !important; + width: 150px !important; +} + +.mat-column-planned { + flex: 0 0 200px !important; + width: 200px !important; +} + +.mat-column-tuning-orb { + flex: 0 0 100px !important; + width: 100px !important; +} + +.mat-column-participants { + flex: 1 1 250px !important; + width: 250px !important; + justify-content: center; +} + +.mat-column-delete { + flex: 1 1 50px !important; + width: 50px !important; + justify-content: right; +} diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.html b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.html index b7d47b0..37bbee2 100644 --- a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.html +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.html @@ -1,43 +1,84 @@ -
-
- - - - Begin - + > + + + Begin + - - - Name - {{row.name}} + + + Name + {{('EXPEDITION.' + row.name) | translate}} - - - Owner - {{row.owner.characterName}} + + + Owner + {{row.owner.characterName}} - - - Participants - + + + Tuning Orbs + {{countTuningOrbs(row)}} + + + + Participants + +
+ + + + + +
+ + + Delete + + + + + + + - - + +
-
diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts index 27ec2a0..703d3d3 100644 --- a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts @@ -1,10 +1,26 @@ import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; import { MatPaginator } from '@angular/material/paginator'; import { MatTableDataSource } from '@angular/material/table'; -import { Expedition } from '@nw-company-tool/model'; +import { Expedition, Participant, Permission, Role } from '@nw-company-tool/model'; import { ExpeditionService } from '../../../../services/expedition/expedition.service'; import { MatDialog } from '@angular/material/dialog'; import { ExpeditionCreateComponent } from '../expedition-create/expedition-create.component'; +import * as moment from 'moment'; +import { + CharacterDetailComponent, + CharacterDetailDialogData +} from '../../../../components/character-detail/character-detail.component'; +import { UserService } from '../../../../services/user/user.service'; +import { Observable, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ExpeditionJoinComponent, ExpeditionJoinDialogData } from '../expedition-join/expedition-join.component'; +import { SnackbarService } from '../../../../services/snackbar/snackbar.service'; + +export type Slot = { + open: boolean; + participant?: Participant; + role: Role; +}; @Component({ selector: 'app-expedition-table', @@ -12,15 +28,23 @@ import { ExpeditionCreateComponent } from '../expedition-create/expedition-creat styleUrls: ['./expedition-table.component.css'] }) export class ExpeditionTableComponent implements OnInit, AfterViewInit { - displayedColumns: string[] = ['planned', 'name', 'owner', 'participants']; + displayedColumns: string[] = ['planned', 'name', 'owner', 'tuning-orb', 'participants', 'delete']; dataSource = new MatTableDataSource(); @ViewChild(MatPaginator) paginator: MatPaginator; - constructor(private expeditionService: ExpeditionService, public dialog: MatDialog) {} + constructor( + private expeditionService: ExpeditionService, + public dialog: MatDialog, + private userService: UserService, + private snackbarService: SnackbarService + ) {} ngOnInit(): void { - this.expeditionService.getExpeditions().subscribe((data) => (this.dataSource.data = data)); + this.expeditionService.getExpeditions().subscribe((data) => { + this.dataSource.data = data; + }); + this.expeditionService.refreshExpeditions(); } ngAfterViewInit(): void { @@ -30,4 +54,141 @@ export class ExpeditionTableComponent implements OnInit, AfterViewInit { createNew(): void { this.dialog.open(ExpeditionCreateComponent); } + + formatDate(date: string): string { + return moment.utc(date).local().format('YYYY-MM-DDTHH:mm:00'); + } + + countTuningOrbs(expedition: Expedition): number { + return expedition.participants.reduce( + (previousValue, currentValue) => previousValue + +currentValue.hasTuningOrb, + 0 + ); + } + + getSlot(participants: Participant[], role: Role, slot = 0): Slot { + const result = participants.filter((participant) => participant.role === role)[slot]; + if (result) { + return { + open: false, + participant: result, + role + }; + } + return { + open: true, + role + }; + } + + getTank(participants: Participant[]): Slot { + return this.getSlot(participants, Role.TANK); + } + + getHeal(participants: Participant[]): Slot { + return this.getSlot(participants, Role.HEAL); + } + + getDamage1(participants: Participant[]): Slot { + return this.getSlot(participants, Role.DAMAGE, 0); + } + + getDamage2(participants: Participant[]): Slot { + return this.getSlot(participants, Role.DAMAGE, 1); + } + + getDamage3(participants: Participant[]): Slot { + return this.getSlot(participants, Role.DAMAGE, 2); + } + + getIcon(role: Role): string { + switch (role) { + case Role.TANK: + return 'shield'; + case Role.DAMAGE: + return 'sports_martial_arts'; + case Role.HEAL: + return 'healing'; + default: + return 'question_mark'; + } + } + + slotClick(slot: Slot, expedition: Expedition) { + this.userService.getUser$().subscribe((user) => { + if (slot.participant) { + if (slot.participant.userId === user.id) { + this.expeditionService.leaveExpedition({ id: expedition.id }); + return; + } + this.showCharacterDetails(slot.participant); + return; + } + + if (slot.open && !expedition.participants.map((participant) => participant.userId).includes(user.id)) { + const data: ExpeditionJoinDialogData = { + slot, + expedition + }; + this.dialog.open(ExpeditionJoinComponent, { data }); + return; + } + + const self = expedition.participants.filter((participant) => participant.userId === user.id)[0]; + if (slot.open && self) { + this.snackbarService.open(`you are already part of this expedition as role ${self.role}!`); + } + }); + } + + showCharacterDetails(participant: Participant): void { + const data: CharacterDetailDialogData = { + id: participant.userId, + characterName: participant.characterName + }; + this.dialog.open(CharacterDetailComponent, { data }); + } + + getSlotColor(slot: Slot): Observable { + return this.userService.getUser$().pipe( + map((user) => { + if (slot.participant?.userId === user.id) return 'primary'; + if (!slot.open) return ''; + return 'accent'; + }) + ); + } + + getUserId(): Observable { + return this.userService.getUser$().pipe(map((user) => user.id)); + } + + delete(expedition: Expedition) { + this.expeditionService.deleteExpedition(expedition.id); + } + + isOwner(expedition: Expedition): Observable { + return this.userService.getUser$().pipe( + map((user) => { + if (user.permissions.includes(Permission.ADMIN)) { + return true; + } + return expedition.owner.userId === user.id; + }) + ); + } + + getSlotTooltip(slot: Slot): Observable { + if (slot.participant) { + return this.userService.getUser$().pipe( + map((user) => { + if (slot.participant.userId === user.id) { + return 'Leave Expedition'; + } + return slot.participant.characterName; + }) + ); + } + return of(`Join as ${slot.role}`); + } } diff --git a/webapp/src/app/pages/expedition/expedition-page.module.ts b/webapp/src/app/pages/expedition/expedition-page.module.ts index 287013b..a21621a 100644 --- a/webapp/src/app/pages/expedition/expedition-page.module.ts +++ b/webapp/src/app/pages/expedition/expedition-page.module.ts @@ -14,6 +14,10 @@ import { MatSelectModule } from '@angular/material/select'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { TranslateModule } from '@ngx-translate/core'; +import { ExpeditionJoinComponent } from './components/expedition-join/expedition-join.component'; @NgModule({ imports: [ @@ -28,9 +32,12 @@ import { ReactiveFormsModule } from '@angular/forms'; MatSelectModule, MatDatepickerModule, MatSlideToggleModule, - ReactiveFormsModule + ReactiveFormsModule, + CommonModule, + MatTooltipModule, + TranslateModule ], - declarations: [ExpeditionComponent, ExpeditionTableComponent, ExpeditionCreateComponent], + declarations: [ExpeditionComponent, ExpeditionTableComponent, ExpeditionCreateComponent, ExpeditionJoinComponent], providers: [], exports: [] }) diff --git a/webapp/src/app/pages/expedition/routes/root/expedition.component.html b/webapp/src/app/pages/expedition/routes/root/expedition.component.html index 9bfaaec..19aae05 100644 --- a/webapp/src/app/pages/expedition/routes/root/expedition.component.html +++ b/webapp/src/app/pages/expedition/routes/root/expedition.component.html @@ -1,7 +1,4 @@
-
-

🚧 UNDER CONSTRUCTION - ONLY USE FOR DEMO PURPOSES 🚧

-
diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.css b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.css index b4f3dac..d5cba1f 100644 --- a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.css +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.css @@ -2,4 +2,20 @@ width: 1024px; } +::ng-deep.calendar-event { + cursor: pointer; +} + +::ng-deep.calendar-event > .fc-event-title { + word-wrap: break-word !important; + white-space: normal !important; + overflow-wrap: break-word; + word-break: break-word; + + -ms-hyphens: auto; + -moz-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts index 2d5b53e..336dd03 100644 --- a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts @@ -1,8 +1,13 @@ import { Component } from '@angular/core'; -import { CalendarOptions } from '@fullcalendar/angular'; +import { CalendarOptions, EventInput } from '@fullcalendar/angular'; import * as moment from 'moment'; import { ExpeditionService } from '../../../../services/expedition/expedition.service'; -import { map } from 'rxjs/operators'; +import { map, mergeMap } from 'rxjs/operators'; +import { EventClickArg, EventSourceInput } from '@fullcalendar/core'; +import { CalendarEventType, Expedition } from '@nw-company-tool/model'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-home-calendar', @@ -15,13 +20,15 @@ export class HomeCalendarComponent { headerToolbar: { left: 'prev,next today', center: 'title', - right: 'dayGridMonth,timeGridWeek,timeGridDay' + right: 'dayGridWeek,dayGridMonth' }, footerToolbar: { left: '', center: '', right: '' }, + initialView: 'dayGridWeek', + nowIndicator: true, aspectRatio: 2, editable: false, selectable: false, @@ -29,25 +36,57 @@ export class HomeCalendarComponent { dayMaxEvents: true, firstDay: 1, locale: navigator.language, - eventSources: [ - { - color: 'yellow', - events: (info, success) => { - this.expeditionService - .getExpeditions() - .pipe( - map((expeditions) => - expeditions.map((expedition) => ({ - title: expedition.name, - start: expedition.beginDateTime - })) - ) - ) - .subscribe((events) => success(events)); - } - } - ] + eventClassNames: 'calendar-event', + eventClick: this.handleEventClick.bind(this), + eventSources: [this.getExpeditions()] }; - constructor(private expeditionService: ExpeditionService) {} + constructor( + private expeditionService: ExpeditionService, + private router: Router, + private translate: TranslateService + ) {} + + private handleEventClick(info: EventClickArg): void { + if (info.event.extendedProps.type === CalendarEventType.EXPEDITION) { + this.router.navigate(['expedition']); + } + } + + private getExpeditions(): EventSourceInput { + return { + color: 'yellow', + events: (info, success) => { + this.expeditionService + .getExpeditions() + .pipe( + map((expeditions) => expeditions.map((expedition) => HomeCalendarComponent.mapExpedition(expedition))), + mergeMap((events) => { + return new Observable((observer) => { + Promise.all( + events.map(async (event) => { + event.title = await this.translate.get(`EXPEDITION.${event.title}`).toPromise(); + return event; + }) + ).then((result) => { + observer.next(result); + observer.complete(); + }); + }); + }) + ) + .subscribe((events) => success(events)); + } + }; + } + + private static mapExpedition(expedition: Expedition): EventInput { + return { + title: expedition.name, + start: expedition.beginDateTime, + extendedProps: { + type: CalendarEventType.EXPEDITION + } + }; + } } diff --git a/webapp/src/app/services/expedition/expedition.service.ts b/webapp/src/app/services/expedition/expedition.service.ts index 01682ab..075af83 100644 --- a/webapp/src/app/services/expedition/expedition.service.ts +++ b/webapp/src/app/services/expedition/expedition.service.ts @@ -1,6 +1,13 @@ import { Injectable } from '@angular/core'; -import { Expedition } from '@nw-company-tool/model'; +import { + CreateExpedition, + DeleteExpedition, + Expedition, + JoinExpedition, + LeaveExpedition +} from '@nw-company-tool/model'; import { Observable, ReplaySubject } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root' @@ -10,16 +17,48 @@ export class ExpeditionService { private expeditions: Expedition[] = []; - constructor() { - this.expeditions$.next(this.expeditions); + constructor(private http: HttpClient) { + this.refreshExpeditions(); } public getExpeditions(): Observable { return this.expeditions$; } - public createExpedition(expedition: Expedition): void { - this.expeditions.push(expedition); - this.expeditions$.next(this.expeditions); + public refreshExpeditions(): void { + this.pullExpeditions().subscribe((result) => { + this.expeditions = result; + this.expeditions$.next(this.expeditions); + }); + } + + private pullExpeditions(): Observable { + return this.http.get('/api/expedition', { withCredentials: true }); + } + + public createExpedition(expedition: CreateExpedition): Observable { + return this.http.post('/api/expedition', expedition, { withCredentials: true }); + } + + public deleteExpedition(id: number): void { + const payload: DeleteExpedition = { + id + }; + this.http.post('/api/expedition/delete', payload, { withCredentials: true }).subscribe(() => { + this.expeditions = this.expeditions.filter((expedition) => expedition.id !== id); + this.expeditions$.next(this.expeditions); + }); + } + + public joinExpedition(joinExpedition: JoinExpedition): void { + this.http.post('/api/expedition/join', joinExpedition, { withCredentials: true }).subscribe(() => { + this.refreshExpeditions(); + }); + } + + public leaveExpedition(leaveExpedition: LeaveExpedition): void { + this.http + .post('/api/expedition/leave', leaveExpedition, { withCredentials: true }) + .subscribe(() => this.refreshExpeditions()); } } diff --git a/webapp/src/app/services/user/user.service.ts b/webapp/src/app/services/user/user.service.ts index 4d86ffa..f41f726 100644 --- a/webapp/src/app/services/user/user.service.ts +++ b/webapp/src/app/services/user/user.service.ts @@ -14,34 +14,33 @@ export class UserService { private avatar$ = new ReplaySubject(1); private loggedIn = false; - constructor(private http: HttpClient, private router: Router) {} + constructor(private http: HttpClient, private router: Router) { + this.loggedIn$.next(false); + } - async login(): Promise { - await this.http - .post('/api/login', {}, { withCredentials: true }) - .pipe( - map((response) => { - if (response.success) { - this.loggedIn = true; - this.loggedIn$.next(true); - this.user$.next(response.user); - this.getAvatar(32).subscribe((avatar) => this.avatar$.next(avatar)); - if (response.user && !response.user.permissions.includes(Permission.ENABLED)) { - this.router.navigate(['account-disabled']); - return; - } + login(): Observable { + return this.http.post('/api/login', {}, { withCredentials: true }).pipe( + map((response) => { + if (response.success) { + this.loggedIn = true; + this.loggedIn$.next(true); + this.user$.next(response.user); + this.getAvatar(32).subscribe((avatar) => this.avatar$.next(avatar)); + if (response.user && !response.user.permissions.includes(Permission.ENABLED)) { + this.router.navigate(['account-disabled']); + return response.user; + } + } else { + if (response.newUser) { + this.router.navigate(['register']); + return undefined; } else { - if (response.newUser) { - this.router.navigate(['register']); - return; - } else { - this.router.navigate(['login']); - return; - } + this.router.navigate(['login']); + return undefined; } - }) - ) - .toPromise(); + } + }) + ); } public getUser$(): Observable { diff --git a/webapp/src/assets/i18n/en.json b/webapp/src/assets/i18n/en.json index 6d6fcec..57450cb 100644 --- a/webapp/src/assets/i18n/en.json +++ b/webapp/src/assets/i18n/en.json @@ -48,6 +48,14 @@ "TRADE_SKILLS": "Trade Skills", "REFINING_SKILLS": "Refining", "GATHERING_SKILLS": "Gathering" + }, + "EXPEDITION": { + "AMRINE_EXCAVATION": "Amrine Excavation", + "STARSTONE_BARROWS": "Starstone Barrows", + "THE_DEPTHS": "The Depths", + "DYNASTY_SHIPYARD": "Dynasty Shipyard", + "LAZARUS_INSTRUMENTALITY": "Lazarus Instrumentality", + "GARDEN_OF_GENESIS": "Garden of Genesis" } } From 230077d3114f51d47aa1325146467537048826f8 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Tue, 30 Nov 2021 21:12:07 +0100 Subject: [PATCH 12/25] fix: only 1 damage role could be assigned to an expedition --- server/src/expedition/expedition.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/expedition/expedition.service.ts b/server/src/expedition/expedition.service.ts index 846569a..04fdc74 100644 --- a/server/src/expedition/expedition.service.ts +++ b/server/src/expedition/expedition.service.ts @@ -81,10 +81,12 @@ export class ExpeditionService { switch (expeditionJoin.role) { case Role.DAMAGE: { if (roles.length >= 3) throw new HttpException('expedition is full', 400); + break; } case Role.TANK: case Role.HEAL: { if (roles.length >= 1) throw new HttpException('expedition is full', 400); + break; } } await this.client.expeditionParticipant.create({ From e1052c74ae1994ad895c6ca07bb8803b427e7dcc Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Tue, 30 Nov 2021 21:17:47 +0100 Subject: [PATCH 13/25] fix: generate prisma client on startup --- server/src/main.ts | 4 ++-- server/src/server.ts | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/server/src/main.ts b/server/src/main.ts index 8bd033f..433a067 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,7 +1,6 @@ import { exec, fork } from 'child_process'; import process from 'process'; -import { Args, ArgsService, Flags } from './args/args.service'; -import Path from 'path'; +import { ArgsService, Flags } from './args/args.service'; const argsService = ArgsService.get(); @@ -55,6 +54,7 @@ async function installDependencies() { console.log('installing dependencies... (this may take a while)'); await run('npm install --only=production'); await run('npm prune'); + await run('npx prisma generate'); console.log('installing dependencies... done.'); } diff --git a/server/src/server.ts b/server/src/server.ts index 79813f6..53c2067 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -15,18 +15,9 @@ const logger = new Logger('NWCT Server'); const argsService = ArgsService.get(); const configService = ConfigService.get(); -async function setupDatabase() { - process.env.DATABASE_URL = `file:${Path.join( - argsService.getArgument(Args.DATAPATH), - configService.getServerConfig().DATABASE, - )}`; - - return await new Promise((resolve) => { - const proc = exec('npx prisma migrate deploy', { - env: { - ...process.env, - }, - }); +function run(command: string): Promise { + return new Promise((resolve) => { + const proc = exec(command); proc.stdout.on('data', (data) => { logger.log(data); @@ -40,6 +31,16 @@ async function setupDatabase() { }); } +async function setupDatabase() { + process.env.DATABASE_URL = `file:${Path.join( + argsService.getArgument(Args.DATAPATH), + configService.getServerConfig().DATABASE, + )}`; + + await run('npx prisma migrate deploy'); + await run('npx prisma generate'); +} + async function bootstrap() { await setupDatabase(); const options: NestApplicationOptions = {}; From f9a37a5f780a67736391eed126281f298ba62b12 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Wed, 1 Dec 2021 08:52:20 +0100 Subject: [PATCH 14/25] fix: expedition caused rxjs issues --- webapp/src/app/app.module.ts | 31 ++++------- .../character-detail.component.ts | 3 +- .../character-detail.module.ts | 14 +++++ .../app/components/footer/footer.module.ts | 9 ++++ .../app/components/header/header.module.ts | 14 +++++ .../src/app/interceptor/interceptor.module.ts | 3 +- .../app/pages/company/company-page.module.ts | 4 +- .../expedition-table.component.ts | 53 ++++++++++--------- .../expedition/expedition-page.module.ts | 8 +-- .../app/services/snackbar/snackbar.module.ts | 5 +- .../app/services/snackbar/snackbar.service.ts | 4 +- 11 files changed, 89 insertions(+), 59 deletions(-) create mode 100644 webapp/src/app/components/character-detail/character-detail.module.ts create mode 100644 webapp/src/app/components/footer/footer.module.ts create mode 100644 webapp/src/app/components/header/header.module.ts diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts index 4c06f1d..755dd69 100644 --- a/webapp/src/app/app.module.ts +++ b/webapp/src/app/app.module.ts @@ -3,18 +3,12 @@ import { BrowserModule } from '@angular/platform-browser'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { HeaderComponent } from './components/header/header.component'; -import { FooterComponent } from './components/footer/footer.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MatButtonModule } from '@angular/material/button'; import { HttpClient, HttpClientModule } from '@angular/common/http'; import { NgcCookieConsentConfig, NgcCookieConsentModule, NgcCookieConsentService } from 'ngx-cookieconsent'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatIconModule } from '@angular/material/icon'; import { LoginGuard } from './guards/login.guard'; import { AdminGuard } from './guards/admin.guard'; import { ConfigService } from './services/config/config.service'; -import { MatMenuModule } from '@angular/material/menu'; import { ReactiveFormsModule } from '@angular/forms'; import { TranslateLoader, TranslateModule, TranslateModuleConfig } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; @@ -24,7 +18,6 @@ import { UserModule } from './services/user/user.module'; import { NavigationModule } from './services/navigation/navigation.module'; import { AdminModule } from './services/admin/admin.module'; import { CharacterModule } from './services/character/character.module'; -import { SnackbarModule } from './services/snackbar/snackbar.module'; import { InterceptorModule } from './interceptor/interceptor.module'; import { MatMomentDateModule } from '@angular/material-moment-adapter'; import { FullCalendarModule } from '@fullcalendar/angular'; @@ -32,9 +25,9 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; import interactionPlugin from '@fullcalendar/interaction'; import { ExpeditionModule } from './services/expedition/expedition.module'; -import { CharacterDetailComponent } from './components/character-detail/character-detail.component'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatDialogModule } from '@angular/material/dialog'; +import { SnackbarModule } from './services/snackbar/snackbar.module'; +import { HeaderModule } from './components/header/header.module'; +import { FooterModule } from './components/footer/footer.module'; const cookieConfig: NgcCookieConsentConfig = { cookie: { @@ -68,7 +61,7 @@ const i18nConfig: TranslateModuleConfig = { FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPlugin]); @NgModule({ - declarations: [AppComponent, HeaderComponent, FooterComponent, CharacterDetailComponent], + declarations: [AppComponent], imports: [ NgcCookieConsentModule.forRoot(cookieConfig), TranslateModule.forRoot(i18nConfig), @@ -77,25 +70,21 @@ FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPl BrowserAnimationsModule, HttpClientModule, ReactiveFormsModule, + MatMomentDateModule, + // Interceptors InterceptorModule, + // Services ConfigModule, - PluginModule, AdminModule, CharacterModule, - ConfigModule, ExpeditionModule, NavigationModule, PluginModule, SnackbarModule, UserModule, - NavigationModule, - MatToolbarModule, - MatMenuModule, - MatIconModule, - MatButtonModule, - MatMomentDateModule, - MatProgressBarModule, - MatDialogModule + // Global Components + HeaderModule, + FooterModule ], providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }, AppComponent, LoginGuard, AdminGuard], bootstrap: [AppComponent], diff --git a/webapp/src/app/components/character-detail/character-detail.component.ts b/webapp/src/app/components/character-detail/character-detail.component.ts index 48c63ba..0f56b6f 100644 --- a/webapp/src/app/components/character-detail/character-detail.component.ts +++ b/webapp/src/app/components/character-detail/character-detail.component.ts @@ -1,5 +1,5 @@ import { Component, Inject, OnInit } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Attribute, Character } from '@nw-company-tool/model'; import { CharacterService } from '../../services/character/character.service'; @@ -60,7 +60,6 @@ export class CharacterDetailComponent implements OnInit { ]; constructor( - public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public dialogData: CharacterDetailDialogData, private characterService: CharacterService ) {} diff --git a/webapp/src/app/components/character-detail/character-detail.module.ts b/webapp/src/app/components/character-detail/character-detail.module.ts new file mode 100644 index 0000000..0c2874c --- /dev/null +++ b/webapp/src/app/components/character-detail/character-detail.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CharacterDetailComponent } from './character-detail.component'; +import { MatDialogModule } from '@angular/material/dialog'; +import { CommonModule } from '@angular/common'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatButtonModule } from '@angular/material/button'; + +@NgModule({ + imports: [MatDialogModule, CommonModule, MatProgressBarModule, TranslateModule, MatButtonModule], + declarations: [CharacterDetailComponent], + exports: [CharacterDetailComponent] +}) +export class CharacterDetailModule {} diff --git a/webapp/src/app/components/footer/footer.module.ts b/webapp/src/app/components/footer/footer.module.ts new file mode 100644 index 0000000..50395dc --- /dev/null +++ b/webapp/src/app/components/footer/footer.module.ts @@ -0,0 +1,9 @@ +import { NgModule } from '@angular/core'; +import { FooterComponent } from './footer.component'; + +@NgModule({ + imports: [], + declarations: [FooterComponent], + exports: [FooterComponent] +}) +export class FooterModule {} diff --git a/webapp/src/app/components/header/header.module.ts b/webapp/src/app/components/header/header.module.ts new file mode 100644 index 0000000..d007596 --- /dev/null +++ b/webapp/src/app/components/header/header.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { HeaderComponent } from './header.component'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { CommonModule } from '@angular/common'; + +@NgModule({ + imports: [MatMenuModule, MatToolbarModule, MatIconModule, MatButtonModule, CommonModule], + declarations: [HeaderComponent], + exports: [HeaderComponent] +}) +export class HeaderModule {} diff --git a/webapp/src/app/interceptor/interceptor.module.ts b/webapp/src/app/interceptor/interceptor.module.ts index 6dcf048..5063f55 100644 --- a/webapp/src/app/interceptor/interceptor.module.ts +++ b/webapp/src/app/interceptor/interceptor.module.ts @@ -1,7 +1,6 @@ import { NgModule } from '@angular/core'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { TokenInterceptor } from './token.interceptor'; -import { SnackbarModule } from '../services/snackbar/snackbar.module'; @NgModule({ providers: [ @@ -11,6 +10,6 @@ import { SnackbarModule } from '../services/snackbar/snackbar.module'; multi: true } ], - imports: [HttpClientModule, SnackbarModule] + imports: [HttpClientModule] }) export class InterceptorModule {} diff --git a/webapp/src/app/pages/company/company-page.module.ts b/webapp/src/app/pages/company/company-page.module.ts index f667a6f..c931c99 100644 --- a/webapp/src/app/pages/company/company-page.module.ts +++ b/webapp/src/app/pages/company/company-page.module.ts @@ -18,6 +18,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { MatCardModule } from '@angular/material/card'; import { MatTabsModule } from '@angular/material/tabs'; import { CommonModule } from '@angular/common'; +import { CharacterDetailModule } from '../../components/character-detail/character-detail.module'; @NgModule({ imports: [ @@ -37,7 +38,8 @@ import { CommonModule } from '@angular/common'; TranslateModule, CommonModule, MatCardModule, - MatTabsModule + MatTabsModule, + CharacterDetailModule ], declarations: [CompanyComponent, CharactersTableComponent], providers: [], diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts index 703d3d3..eba3392 100644 --- a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts @@ -12,7 +12,7 @@ import { } from '../../../../components/character-detail/character-detail.component'; import { UserService } from '../../../../services/user/user.service'; import { Observable, of } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { first, map } from 'rxjs/operators'; import { ExpeditionJoinComponent, ExpeditionJoinDialogData } from '../expedition-join/expedition-join.component'; import { SnackbarService } from '../../../../services/snackbar/snackbar.service'; @@ -35,7 +35,7 @@ export class ExpeditionTableComponent implements OnInit, AfterViewInit { constructor( private expeditionService: ExpeditionService, - public dialog: MatDialog, + private dialog: MatDialog, private userService: UserService, private snackbarService: SnackbarService ) {} @@ -114,31 +114,34 @@ export class ExpeditionTableComponent implements OnInit, AfterViewInit { } } - slotClick(slot: Slot, expedition: Expedition) { - this.userService.getUser$().subscribe((user) => { - if (slot.participant) { - if (slot.participant.userId === user.id) { - this.expeditionService.leaveExpedition({ id: expedition.id }); + slotClick(slot: Slot, expedition: Expedition): void { + this.userService + .getUser$() + .pipe(first()) + .subscribe((user) => { + if (slot.participant) { + if (slot.participant.userId === user.id) { + this.expeditionService.leaveExpedition({ id: expedition.id }); + return; + } + this.showCharacterDetails(slot.participant); return; } - this.showCharacterDetails(slot.participant); - return; - } - - if (slot.open && !expedition.participants.map((participant) => participant.userId).includes(user.id)) { - const data: ExpeditionJoinDialogData = { - slot, - expedition - }; - this.dialog.open(ExpeditionJoinComponent, { data }); - return; - } - - const self = expedition.participants.filter((participant) => participant.userId === user.id)[0]; - if (slot.open && self) { - this.snackbarService.open(`you are already part of this expedition as role ${self.role}!`); - } - }); + + if (slot.open && !expedition.participants.map((participant) => participant.userId).includes(user.id)) { + const data: ExpeditionJoinDialogData = { + slot, + expedition + }; + this.dialog.open(ExpeditionJoinComponent, { data }); + return; + } + + const self = expedition.participants.filter((participant) => participant.userId === user.id)[0]; + if (slot.open && self) { + this.snackbarService.open(`you are already part of this expedition as role ${self.role}!`); + } + }); } showCharacterDetails(participant: Participant): void { diff --git a/webapp/src/app/pages/expedition/expedition-page.module.ts b/webapp/src/app/pages/expedition/expedition-page.module.ts index a21621a..259146d 100644 --- a/webapp/src/app/pages/expedition/expedition-page.module.ts +++ b/webapp/src/app/pages/expedition/expedition-page.module.ts @@ -18,24 +18,26 @@ import { CommonModule } from '@angular/common'; import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { ExpeditionJoinComponent } from './components/expedition-join/expedition-join.component'; +import { CharacterDetailModule } from '../../components/character-detail/character-detail.module'; @NgModule({ imports: [ ExpeditionRoutingModule, + MatDialogModule, + CommonModule, MatCardModule, MatButtonModule, MatTableModule, MatPaginatorModule, MatInputModule, MatIconModule, - MatDialogModule, MatSelectModule, MatDatepickerModule, MatSlideToggleModule, ReactiveFormsModule, - CommonModule, MatTooltipModule, - TranslateModule + TranslateModule, + CharacterDetailModule ], declarations: [ExpeditionComponent, ExpeditionTableComponent, ExpeditionCreateComponent, ExpeditionJoinComponent], providers: [], diff --git a/webapp/src/app/services/snackbar/snackbar.module.ts b/webapp/src/app/services/snackbar/snackbar.module.ts index f592927..1a9f428 100644 --- a/webapp/src/app/services/snackbar/snackbar.module.ts +++ b/webapp/src/app/services/snackbar/snackbar.module.ts @@ -1,11 +1,10 @@ import { NgModule } from '@angular/core'; -import { HttpClientModule } from '@angular/common/http'; import { SnackbarService } from './snackbar.service'; import { MatSnackBarModule } from '@angular/material/snack-bar'; @NgModule({ providers: [SnackbarService], - imports: [HttpClientModule, MatSnackBarModule], - exports: [MatSnackBarModule] + imports: [MatSnackBarModule], + exports: [] }) export class SnackbarModule {} diff --git a/webapp/src/app/services/snackbar/snackbar.service.ts b/webapp/src/app/services/snackbar/snackbar.service.ts index 5e8371b..e1b5874 100644 --- a/webapp/src/app/services/snackbar/snackbar.service.ts +++ b/webapp/src/app/services/snackbar/snackbar.service.ts @@ -19,8 +19,8 @@ export class SnackbarService { error(message: string, duration = 3000): void { this.snackBar.open(message, 'OK', { duration, - horizontalPosition: 'start', - verticalPosition: 'bottom', + horizontalPosition: 'center', + verticalPosition: 'top', panelClass: ['snackbar-error'] }); } From 99e4d4089feadba26116acbc91d1104192f3d923 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Wed, 1 Dec 2021 19:43:51 +0100 Subject: [PATCH 15/25] fix: logout if user not found --- server/src/user/user.service.ts | 32 +++++++++++++++++--------------- server/webpack.config.js | 1 + 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index 13112fc..019a581 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -38,21 +38,23 @@ export class UserService { .then((permissions) => permissions.map((permission) => permission.permission)); } - async findUserWithPermissionsById(id: number): Promise { - return this.client.user - .findUnique({ - where: { id }, - include: { - UserPermission: true, - }, - }) - .then((result) => ({ - id: result.id, - discordId: result.discordId, - discordUsername: result.discordUsername, - characterName: result.characterName, - permissions: result.UserPermission.map((it) => it.permission as Permission), - })); + async findUserWithPermissionsById(id: number): Promise { + const user = await this.client.user.findUnique({ + where: { id }, + include: { + UserPermission: true, + }, + }); + if (!user) { + return undefined; + } + return { + id: user.id, + discordId: user.discordId, + discordUsername: user.discordUsername, + characterName: user.characterName, + permissions: user.UserPermission.map((it) => it.permission as Permission), + }; } async findByDiscordId(discordId: string): Promise { diff --git a/server/webpack.config.js b/server/webpack.config.js index 955f3cd..ac23bd7 100644 --- a/server/webpack.config.js +++ b/server/webpack.config.js @@ -11,5 +11,6 @@ module.exports = function (options) { path: path.resolve(__dirname, 'dist'), filename: '[name].js', }, + devtool: 'source-map', }; }; From 874115646e4fd6a0bc67511fd0150cf3b920c17d Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Wed, 1 Dec 2021 20:24:13 +0100 Subject: [PATCH 16/25] fix(#31): users could not use spaces in their character names --- server/src/login/dto/user.create.dto.ts | 8 +++++--- server/src/user/dto/charactername.set.dto.ts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/server/src/login/dto/user.create.dto.ts b/server/src/login/dto/user.create.dto.ts index 24617b6..cd0e57d 100644 --- a/server/src/login/dto/user.create.dto.ts +++ b/server/src/login/dto/user.create.dto.ts @@ -1,8 +1,10 @@ -import { IsAlpha, IsNotEmpty } from 'class-validator'; +import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; import { CreateUser } from '@nw-company-tool/model'; export class CreateUserDto implements CreateUser { - @IsAlpha() - @IsNotEmpty() + @Matches(/^[a-zA-Z0-9\s]+$/) + @MinLength(2) + @MaxLength(24) + @IsString() characterName: string; } diff --git a/server/src/user/dto/charactername.set.dto.ts b/server/src/user/dto/charactername.set.dto.ts index 7143418..e498ea1 100644 --- a/server/src/user/dto/charactername.set.dto.ts +++ b/server/src/user/dto/charactername.set.dto.ts @@ -1,8 +1,10 @@ -import { IsAlpha, IsNotEmpty } from 'class-validator'; +import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; import { SetCharacterName } from '@nw-company-tool/model'; export class SetCharacterNameDto implements SetCharacterName { - @IsAlpha() - @IsNotEmpty() + @Matches(/^[a-zA-Z0-9\s]+$/) + @MinLength(2) + @MaxLength(24) + @IsString() characterName: string; } From 9a1b8d099fdcc5c0a8f06202730f207e7dd456b5 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Wed, 1 Dec 2021 20:34:53 +0100 Subject: [PATCH 17/25] fix: menu was not working --- webapp/src/app/components/header/header.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/src/app/components/header/header.module.ts b/webapp/src/app/components/header/header.module.ts index d007596..b3d1172 100644 --- a/webapp/src/app/components/header/header.module.ts +++ b/webapp/src/app/components/header/header.module.ts @@ -5,9 +5,10 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; @NgModule({ - imports: [MatMenuModule, MatToolbarModule, MatIconModule, MatButtonModule, CommonModule], + imports: [MatMenuModule, MatToolbarModule, MatIconModule, MatButtonModule, CommonModule, RouterModule], declarations: [HeaderComponent], exports: [HeaderComponent] }) From 8a504fef6b8bdbd8fbd4cec68e281d5aa2c236cf Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Fri, 3 Dec 2021 12:20:16 +0100 Subject: [PATCH 18/25] feat: expeditions now stay in sync without the need to reload manually. This features uses server sent events as is used to evaluate if and how the event architecture works --- libs/model/src/event.model.ts | 7 ++++ libs/model/src/expedition.model.ts | 22 ++++++++++ libs/model/src/index.ts | 1 + server/package-lock.json | 32 ++++++++++++++ server/package.json | 17 +++++--- server/src/app.module.ts | 6 +++ server/src/event/event.controller.ts | 23 ++++++++++ server/src/event/event.module.ts | 11 +++++ server/src/event/event.service.ts | 12 ++++++ server/src/expedition/expedition.module.ts | 3 +- server/src/expedition/expedition.service.ts | 16 ++++++- webapp/src/app/app.component.ts | 5 ++- .../expedition-create.component.ts | 1 - .../home-calendar.component.html | 2 +- .../home-calendar/home-calendar.component.ts | 25 ++++++++--- webapp/src/app/services/event/event.module.ts | 8 ++++ .../src/app/services/event/event.service.ts | 40 ++++++++++++++++++ .../services/expedition/expedition.service.ts | 42 ++++++++++++++++--- 18 files changed, 250 insertions(+), 23 deletions(-) create mode 100644 libs/model/src/event.model.ts create mode 100644 server/src/event/event.controller.ts create mode 100644 server/src/event/event.module.ts create mode 100644 server/src/event/event.service.ts create mode 100644 webapp/src/app/services/event/event.module.ts create mode 100644 webapp/src/app/services/event/event.service.ts diff --git a/libs/model/src/event.model.ts b/libs/model/src/event.model.ts new file mode 100644 index 0000000..c6d2c59 --- /dev/null +++ b/libs/model/src/event.model.ts @@ -0,0 +1,7 @@ +export type EventId = string; + +export interface Event { + id: EventId; +} + +export type EventType = new (...params: never[]) => T; diff --git a/libs/model/src/expedition.model.ts b/libs/model/src/expedition.model.ts index 58e902e..95896ad 100644 --- a/libs/model/src/expedition.model.ts +++ b/libs/model/src/expedition.model.ts @@ -1,3 +1,5 @@ +import { Event } from './event.model'; + export type CreateExpedition = { name: ExpeditionName; beginDateTime: string; @@ -55,3 +57,23 @@ export enum ExpeditionName { LAZARUS_INSTRUMENTALITY = 'LAZARUS_INSTRUMENTALITY', GARDEN_OF_GENESIS = 'GARDEN_OF_GENESIS', } + +export class ExpeditionCreateEvent implements Event { + id = 'EXPEDITION.CREATE'; + constructor(public expedition: Expedition) {} +} + +export class ExpeditionDeleteEvent implements Event { + id = 'EXPEDITION.DELETE'; + constructor(public expeditionId: number) {} +} + +export class ExpeditionJoinEvent implements Event { + id = 'EXPEDITION.JOIN'; + constructor(public expedition: Expedition) {} +} + +export class ExpeditionLeaveEvent implements Event { + id = 'EXPEDITION.LEAVE'; + constructor(public expedition: Expedition) {} +} diff --git a/libs/model/src/index.ts b/libs/model/src/index.ts index dc30237..cd8ef9d 100644 --- a/libs/model/src/index.ts +++ b/libs/model/src/index.ts @@ -3,6 +3,7 @@ export * from './calendar.model'; export * from './character.model'; export * from './config.model'; export * from './discord.model'; +export * from './event.model'; export * from './expedition.model'; export * from './github.model'; export * from './login.model'; diff --git a/server/package-lock.json b/server/package-lock.json index 45d7b43..22eb302 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,6 +12,7 @@ "@nestjs/axios": "^0.0.3", "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", + "@nestjs/event-emitter": "^1.0.0", "@nestjs/platform-express": "^8.0.0", "@nestjs/serve-static": "^2.2.2", "@prisma/client": "^3.5.0", @@ -1545,6 +1546,19 @@ } } }, + "node_modules/@nestjs/event-emitter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-1.0.0.tgz", + "integrity": "sha512-dRAou6G89KKYI2iyYfqSVGE6ZTC4WmHkQkFfgh88GLQg8dBqRk92ZY8CRtL2SK32SSelh9bwEDNQn9561uoypA==", + "dependencies": { + "eventemitter2": "6.4.4" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0", + "reflect-metadata": "^0.1.12" + } + }, "node_modules/@nestjs/platform-express": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-8.2.0.tgz", @@ -4155,6 +4169,11 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter2": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz", + "integrity": "sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -10311,6 +10330,14 @@ "uuid": "8.3.2" } }, + "@nestjs/event-emitter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-1.0.0.tgz", + "integrity": "sha512-dRAou6G89KKYI2iyYfqSVGE6ZTC4WmHkQkFfgh88GLQg8dBqRk92ZY8CRtL2SK32SSelh9bwEDNQn9561uoypA==", + "requires": { + "eventemitter2": "6.4.4" + } + }, "@nestjs/platform-express": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-8.2.0.tgz", @@ -12349,6 +12376,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "eventemitter2": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.4.tgz", + "integrity": "sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", diff --git a/server/package.json b/server/package.json index 1640f4c..7ef9f41 100644 --- a/server/package.json +++ b/server/package.json @@ -3,7 +3,12 @@ "version": "0.0.0", "description": "server for the new world company tool", "author": "Krise", - "keywords": ["New World", "Company", "Tool", "Gaming"], + "keywords": [ + "New World", + "Company", + "Tool", + "Gaming" + ], "bugs": { "url": "https://github.com/cbartel/nw-company-tool/issues" }, @@ -32,23 +37,23 @@ "@nestjs/axios": "^0.0.3", "@nestjs/common": "^8.0.0", "@nestjs/core": "^8.0.0", + "@nestjs/event-emitter": "^1.0.0", "@nestjs/platform-express": "^8.0.0", "@nestjs/serve-static": "^2.2.2", "@prisma/client": "^3.5.0", - "prisma": "^3.5.0", "class-transformer": "^0.4.0", "class-validator": "^0.13.1", "cookie-parser": "^1.4.5", "fs-extra": "^10.0.0", "jsonwebtoken": "^8.5.1", + "prisma": "^3.5.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", + "semver": "^7.3.5", "tar-stream": "^2.2.0", "tslib": "^2.3.0", - "zlib": "^1.0.5", - "semver": "^7.3.5" - + "zlib": "^1.0.5" }, "devDependencies": { "@nestjs/cli": "^8.0.0", @@ -59,8 +64,8 @@ "@types/express": "^4.17.13", "@types/jest": "^27.0.1", "@types/node": "^16.0.0", - "@types/supertest": "^2.0.11", "@types/semver": "^7.3.9", + "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index aedce38..a7fd49f 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -10,13 +10,19 @@ import { CharacterModule } from './character/character.module'; import { PluginModule } from './plugin/plugin.module'; import { FrontendMiddleware } from './middleware/frontend.middleware'; import { ExpeditionModule } from './expedition/expedition.module'; +import { EventModule } from './event/event.module'; +import { EventEmitterModule } from '@nestjs/event-emitter'; @Module({ imports: [ + EventEmitterModule.forRoot({ + wildcard: true, + }), TokenModule, ConfigModule, ArgsModule, DatabaseModule, + EventModule, ExpeditionModule, UserModule, LoginModule, diff --git a/server/src/event/event.controller.ts b/server/src/event/event.controller.ts new file mode 100644 index 0000000..7fa0a19 --- /dev/null +++ b/server/src/event/event.controller.ts @@ -0,0 +1,23 @@ +import { Controller, Sse } from '@nestjs/common'; +import { Observable, Subject } from 'rxjs'; +import { RequiredPermissions } from '../login/login.decorator'; +import { Permission } from '@nw-company-tool/model'; +import { OnEvent } from '@nestjs/event-emitter'; + +@Controller('/api/event') +@RequiredPermissions(Permission.ENABLED) +export class EventController { + private event$ = new Subject(); + + @Sse('/') + sse(): Observable { + return new Observable((observer) => { + this.event$.subscribe((event) => observer.next(event)); + }); + } + + @OnEvent('**') + onEvent(event: Event): void { + this.event$.next(JSON.stringify(event)); + } +} diff --git a/server/src/event/event.module.ts b/server/src/event/event.module.ts new file mode 100644 index 0000000..61b81e9 --- /dev/null +++ b/server/src/event/event.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { EventController } from './event.controller'; +import { EventService } from './event.service'; + +@Module({ + imports: [], + providers: [EventService], + controllers: [EventController], + exports: [EventService], +}) +export class EventModule {} diff --git a/server/src/event/event.service.ts b/server/src/event/event.service.ts new file mode 100644 index 0000000..915cbdd --- /dev/null +++ b/server/src/event/event.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Event } from '@nw-company-tool/model'; + +@Injectable() +export class EventService { + constructor(private eventEmitter: EventEmitter2) {} + + emit(event: Event): void { + this.eventEmitter.emit(event.id, event); + } +} diff --git a/server/src/expedition/expedition.module.ts b/server/src/expedition/expedition.module.ts index aea797a..e56aef1 100644 --- a/server/src/expedition/expedition.module.ts +++ b/server/src/expedition/expedition.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { DatabaseModule } from '../database/database.module'; import { ExpeditionController } from './expedition.controller'; import { ExpeditionService } from './expedition.service'; +import { EventModule } from '../event/event.module'; @Module({ - imports: [DatabaseModule], + imports: [DatabaseModule, EventModule], controllers: [ExpeditionController], providers: [ExpeditionService], exports: [], diff --git a/server/src/expedition/expedition.service.ts b/server/src/expedition/expedition.service.ts index 04fdc74..bb15f49 100644 --- a/server/src/expedition/expedition.service.ts +++ b/server/src/expedition/expedition.service.ts @@ -2,6 +2,10 @@ import { HttpException, Injectable } from '@nestjs/common'; import { CreateExpedition, Expedition, + ExpeditionCreateEvent, + ExpeditionDeleteEvent, + ExpeditionJoinEvent, + ExpeditionLeaveEvent, JoinExpedition, LeaveExpedition, Participant, @@ -10,6 +14,7 @@ import { } from '@nw-company-tool/model'; import { ExpeditionParticipant } from '@prisma/client'; import { DatabaseClient } from '../database/database.client'; +import { EventService } from '../event/event.service'; type ExpeditionQueryResult = { id: number; @@ -21,7 +26,7 @@ type ExpeditionQueryResult = { @Injectable() export class ExpeditionService { - constructor(private client: DatabaseClient) {} + constructor(private client: DatabaseClient, private eventService: EventService) {} findAll(): Promise { return this.client.expedition @@ -97,6 +102,8 @@ export class ExpeditionService { role: expeditionJoin.role, }, }); + const expedition = await this.findById(expeditionJoin.id); + this.eventService.emit(new ExpeditionJoinEvent(expedition)); } async leave(user: User, expeditionLeave: LeaveExpedition): Promise { @@ -108,10 +115,12 @@ export class ExpeditionService { }, }, }); + const expedition = await this.findById(expeditionLeave.id); + this.eventService.emit(new ExpeditionLeaveEvent(expedition)); } async create(user: User, expeditionData: CreateExpedition): Promise { - await this.client.expedition.create({ + const createResult = await this.client.expedition.create({ data: { name: expeditionData.name, beginDateTime: expeditionData.beginDateTime, @@ -129,6 +138,8 @@ export class ExpeditionService { }, }, }); + const expedition = await this.findById(createResult.id); + this.eventService.emit(new ExpeditionCreateEvent(expedition)); } async delete(id: number): Promise { @@ -137,6 +148,7 @@ export class ExpeditionService { id, }, }); + this.eventService.emit(new ExpeditionDeleteEvent(id)); } private mapToExpedition(result: ExpeditionQueryResult): Expedition { diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts index 27e0a2d..d9430e9 100644 --- a/webapp/src/app/app.component.ts +++ b/webapp/src/app/app.component.ts @@ -4,7 +4,7 @@ import { Router, Routes } from '@angular/router'; import { PluginDefinition } from './services/plugin/plugin.model'; import { loadRemoteModule } from '@angular-architects/module-federation'; import { routes } from './app-routing.module'; -import { UserService } from './services/user/user.service'; +import { EventService } from './services/event/event.service'; @Component({ selector: 'app-root', @@ -12,12 +12,13 @@ import { UserService } from './services/user/user.service'; styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { - constructor(private userService: UserService, private router: Router, private pluginService: PluginService) {} + constructor(private router: Router, private pluginService: PluginService, private eventService: EventService) {} async ngOnInit(): Promise { const plugins = await this.pluginService.getPlugins().toPromise(); const routes = this.buildRoutes(plugins); this.router.resetConfig(routes); + this.eventService.init(); } buildRoutes(plugins: PluginDefinition[]): Routes { diff --git a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts index 02b08bd..622b3a2 100644 --- a/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts +++ b/webapp/src/app/pages/expedition/components/expedition-create/expedition-create.component.ts @@ -52,7 +52,6 @@ export class ExpeditionCreateComponent { role: formData.role }) .subscribe(() => { - this.expeditionService.refreshExpeditions(); this.dialogRef.close(); }); } diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.html b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.html index 5599a58..32e50e9 100644 --- a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.html +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.html @@ -1,7 +1,7 @@
- +
diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts index 336dd03..4b9fb74 100644 --- a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts @@ -1,20 +1,20 @@ -import { Component } from '@angular/core'; -import { CalendarOptions, EventInput } from '@fullcalendar/angular'; +import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { CalendarOptions, EventInput, FullCalendarComponent } from '@fullcalendar/angular'; import * as moment from 'moment'; import { ExpeditionService } from '../../../../services/expedition/expedition.service'; -import { map, mergeMap } from 'rxjs/operators'; +import { first, map, mergeMap } from 'rxjs/operators'; import { EventClickArg, EventSourceInput } from '@fullcalendar/core'; import { CalendarEventType, Expedition } from '@nw-company-tool/model'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { Observable } from 'rxjs'; +import { Observable, Subscription } from 'rxjs'; @Component({ selector: 'app-home-calendar', templateUrl: './home-calendar.component.html', styleUrls: ['./home-calendar.component.css'] }) -export class HomeCalendarComponent { +export class HomeCalendarComponent implements AfterViewInit, OnDestroy { options: CalendarOptions = { initialDate: moment().format('yyyy-MM-DD'), headerToolbar: { @@ -41,6 +41,10 @@ export class HomeCalendarComponent { eventSources: [this.getExpeditions()] }; + updateSubscription: Subscription; + + @ViewChild('calendar') calendarComponent: FullCalendarComponent; + constructor( private expeditionService: ExpeditionService, private router: Router, @@ -60,6 +64,7 @@ export class HomeCalendarComponent { this.expeditionService .getExpeditions() .pipe( + first(), map((expeditions) => expeditions.map((expedition) => HomeCalendarComponent.mapExpedition(expedition))), mergeMap((events) => { return new Observable((observer) => { @@ -89,4 +94,14 @@ export class HomeCalendarComponent { } }; } + + ngAfterViewInit(): void { + this.updateSubscription = this.expeditionService + .getExpeditions() + .subscribe(() => this.calendarComponent.getApi().refetchEvents()); + } + + ngOnDestroy(): void { + this.updateSubscription?.unsubscribe(); + } } diff --git a/webapp/src/app/services/event/event.module.ts b/webapp/src/app/services/event/event.module.ts new file mode 100644 index 0000000..51a5620 --- /dev/null +++ b/webapp/src/app/services/event/event.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core'; +import { EventService } from './event.service'; + +@NgModule({ + imports: [], + providers: [EventService] +}) +export class EventModule {} diff --git a/webapp/src/app/services/event/event.service.ts b/webapp/src/app/services/event/event.service.ts new file mode 100644 index 0000000..c4d3bfb --- /dev/null +++ b/webapp/src/app/services/event/event.service.ts @@ -0,0 +1,40 @@ +import { Injectable, NgZone } from '@angular/core'; +import { UserService } from '../user/user.service'; +import { Event, EventType } from '@nw-company-tool/model'; +import { Observable, Subject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class EventService { + private eventSource: EventSource; + private events$ = new Subject(); + + constructor(private zone: NgZone, private userService: UserService) {} + + init(): void { + this.userService.isLoggedIn$().subscribe((loggedIn) => { + if (loggedIn) { + this.eventSource = new EventSource('/api/event', { withCredentials: true }); + this.eventSource.onmessage = (event) => { + this.zone.run(() => this.onEvent(JSON.parse(event.data))); + }; + } else { + this.eventSource?.close(); + } + }); + } + + private onEvent(event: Event) { + this.events$.next(event); + } + + subscribe(eventType: EventType): Observable { + const eventId = new eventType().id; + return this.events$.pipe( + filter((event) => event.id === eventId), + map((event) => event as T) + ); + } +} diff --git a/webapp/src/app/services/expedition/expedition.service.ts b/webapp/src/app/services/expedition/expedition.service.ts index 075af83..2e94ae3 100644 --- a/webapp/src/app/services/expedition/expedition.service.ts +++ b/webapp/src/app/services/expedition/expedition.service.ts @@ -3,11 +3,16 @@ import { CreateExpedition, DeleteExpedition, Expedition, + ExpeditionCreateEvent, + ExpeditionDeleteEvent, + ExpeditionJoinEvent, + ExpeditionLeaveEvent, JoinExpedition, LeaveExpedition } from '@nw-company-tool/model'; import { Observable, ReplaySubject } from 'rxjs'; import { HttpClient } from '@angular/common/http'; +import { EventService } from '../event/event.service'; @Injectable({ providedIn: 'root' @@ -17,7 +22,11 @@ export class ExpeditionService { private expeditions: Expedition[] = []; - constructor(private http: HttpClient) { + constructor(private http: HttpClient, private eventService: EventService) { + this.eventService.subscribe(ExpeditionCreateEvent).subscribe((event) => this.onExpeditionCreate(event)); + this.eventService.subscribe(ExpeditionDeleteEvent).subscribe((event) => this.onExpeditionDelete(event)); + this.eventService.subscribe(ExpeditionJoinEvent).subscribe((event) => this.onExpeditionJoin(event)); + this.eventService.subscribe(ExpeditionLeaveEvent).subscribe((event) => this.onExpeditionLeave(event)); this.refreshExpeditions(); } @@ -44,10 +53,7 @@ export class ExpeditionService { const payload: DeleteExpedition = { id }; - this.http.post('/api/expedition/delete', payload, { withCredentials: true }).subscribe(() => { - this.expeditions = this.expeditions.filter((expedition) => expedition.id !== id); - this.expeditions$.next(this.expeditions); - }); + this.http.post('/api/expedition/delete', payload, { withCredentials: true }).subscribe(); } public joinExpedition(joinExpedition: JoinExpedition): void { @@ -61,4 +67,30 @@ export class ExpeditionService { .post('/api/expedition/leave', leaveExpedition, { withCredentials: true }) .subscribe(() => this.refreshExpeditions()); } + + private onExpeditionCreate(event: ExpeditionCreateEvent): void { + this.expeditions.push(event.expedition); + this.expeditions$.next(this.expeditions); + } + + private onExpeditionDelete(event: ExpeditionDeleteEvent): void { + this.expeditions = this.expeditions.filter((expedition) => expedition.id !== event.expeditionId); + this.expeditions$.next(this.expeditions); + } + + private onExpeditionJoin(event: ExpeditionJoinEvent): void { + this.updateExpedition(event.expedition); + } + + private onExpeditionLeave(event: ExpeditionLeaveEvent): void { + this.updateExpedition(event.expedition); + } + + private updateExpedition(expedition: Expedition): void { + const idx = this.expeditions.findIndex((expedition) => expedition.id === expedition.id); + if (idx >= 0) { + this.expeditions[idx] = expedition; + } + this.expeditions$.next(this.expeditions); + } } From eae55c7b2718fe3a8719e0903e7888f3cb5b6542 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Fri, 3 Dec 2021 13:29:42 +0100 Subject: [PATCH 19/25] fix: expeditions were not updated correctly --- .../expedition-table/expedition-table.component.ts | 1 - webapp/src/app/services/expedition/expedition.service.ts | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts index eba3392..2a64f7d 100644 --- a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts @@ -44,7 +44,6 @@ export class ExpeditionTableComponent implements OnInit, AfterViewInit { this.expeditionService.getExpeditions().subscribe((data) => { this.dataSource.data = data; }); - this.expeditionService.refreshExpeditions(); } ngAfterViewInit(): void { diff --git a/webapp/src/app/services/expedition/expedition.service.ts b/webapp/src/app/services/expedition/expedition.service.ts index 2e94ae3..f1425b6 100644 --- a/webapp/src/app/services/expedition/expedition.service.ts +++ b/webapp/src/app/services/expedition/expedition.service.ts @@ -86,10 +86,10 @@ export class ExpeditionService { this.updateExpedition(event.expedition); } - private updateExpedition(expedition: Expedition): void { - const idx = this.expeditions.findIndex((expedition) => expedition.id === expedition.id); + private updateExpedition(updatedExpedition: Expedition): void { + const idx = this.expeditions.findIndex((expedition) => expedition.id === updatedExpedition.id); if (idx >= 0) { - this.expeditions[idx] = expedition; + this.expeditions[idx] = updatedExpedition; } this.expeditions$.next(this.expeditions); } From 463a62e19a789f8f820071023c1340cb1a1e49a2 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Fri, 3 Dec 2021 14:14:38 +0100 Subject: [PATCH 20/25] feat: SSE reconnect after disconnect --- libs/model/src/event.model.ts | 4 ++ server/src/event/event.controller.ts | 8 ++- webapp/src/app/app.component.ts | 4 +- webapp/src/app/app.module.ts | 7 +- .../expedition-table.component.ts | 1 + .../home-calendar/home-calendar.component.ts | 28 ++++---- .../src/app/services/event/event.service.ts | 66 ++++++++++++++++--- .../services/expedition/expedition.service.ts | 1 - 8 files changed, 90 insertions(+), 29 deletions(-) diff --git a/libs/model/src/event.model.ts b/libs/model/src/event.model.ts index c6d2c59..af4593b 100644 --- a/libs/model/src/event.model.ts +++ b/libs/model/src/event.model.ts @@ -5,3 +5,7 @@ export interface Event { } export type EventType = new (...params: never[]) => T; + +export class KeepAliveEvent implements Event { + id: 'KEEPALIVE'; +} diff --git a/server/src/event/event.controller.ts b/server/src/event/event.controller.ts index 7fa0a19..0eb777d 100644 --- a/server/src/event/event.controller.ts +++ b/server/src/event/event.controller.ts @@ -1,7 +1,7 @@ import { Controller, Sse } from '@nestjs/common'; -import { Observable, Subject } from 'rxjs'; +import { interval, Observable, Subject } from 'rxjs'; import { RequiredPermissions } from '../login/login.decorator'; -import { Permission } from '@nw-company-tool/model'; +import { KeepAliveEvent, Permission } from '@nw-company-tool/model'; import { OnEvent } from '@nestjs/event-emitter'; @Controller('/api/event') @@ -9,6 +9,10 @@ import { OnEvent } from '@nestjs/event-emitter'; export class EventController { private event$ = new Subject(); + constructor() { + interval(15000).subscribe(() => this.event$.next(JSON.stringify(new KeepAliveEvent()))); + } + @Sse('/') sse(): Observable { return new Observable((observer) => { diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts index d9430e9..18be43e 100644 --- a/webapp/src/app/app.component.ts +++ b/webapp/src/app/app.component.ts @@ -4,7 +4,6 @@ import { Router, Routes } from '@angular/router'; import { PluginDefinition } from './services/plugin/plugin.model'; import { loadRemoteModule } from '@angular-architects/module-federation'; import { routes } from './app-routing.module'; -import { EventService } from './services/event/event.service'; @Component({ selector: 'app-root', @@ -12,13 +11,12 @@ import { EventService } from './services/event/event.service'; styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { - constructor(private router: Router, private pluginService: PluginService, private eventService: EventService) {} + constructor(private router: Router, private pluginService: PluginService) {} async ngOnInit(): Promise { const plugins = await this.pluginService.getPlugins().toPromise(); const routes = this.buildRoutes(plugins); this.router.resetConfig(routes); - this.eventService.init(); } buildRoutes(plugins: PluginDefinition[]): Routes { diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts index 755dd69..5b008a0 100644 --- a/webapp/src/app/app.module.ts +++ b/webapp/src/app/app.module.ts @@ -28,6 +28,7 @@ import { ExpeditionModule } from './services/expedition/expedition.module'; import { SnackbarModule } from './services/snackbar/snackbar.module'; import { HeaderModule } from './components/header/header.module'; import { FooterModule } from './components/footer/footer.module'; +import { EventService } from './services/event/event.service'; const cookieConfig: NgcCookieConsentConfig = { cookie: { @@ -91,5 +92,9 @@ FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPl exports: [] }) export class AppModule { - constructor(private ccService: NgcCookieConsentService, private configService: ConfigService) {} + constructor( + private ccService: NgcCookieConsentService, + private configService: ConfigService, + private eventService: EventService + ) {} } diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts index 2a64f7d..eba3392 100644 --- a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts @@ -44,6 +44,7 @@ export class ExpeditionTableComponent implements OnInit, AfterViewInit { this.expeditionService.getExpeditions().subscribe((data) => { this.dataSource.data = data; }); + this.expeditionService.refreshExpeditions(); } ngAfterViewInit(): void { diff --git a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts index 4b9fb74..743b096 100644 --- a/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts +++ b/webapp/src/app/pages/home/components/home-calendar/home-calendar.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { CalendarOptions, EventInput, FullCalendarComponent } from '@fullcalendar/angular'; import * as moment from 'moment'; import { ExpeditionService } from '../../../../services/expedition/expedition.service'; @@ -14,7 +14,7 @@ import { Observable, Subscription } from 'rxjs'; templateUrl: './home-calendar.component.html', styleUrls: ['./home-calendar.component.css'] }) -export class HomeCalendarComponent implements AfterViewInit, OnDestroy { +export class HomeCalendarComponent implements OnInit, AfterViewInit, OnDestroy { options: CalendarOptions = { initialDate: moment().format('yyyy-MM-DD'), headerToolbar: { @@ -51,6 +51,20 @@ export class HomeCalendarComponent implements AfterViewInit, OnDestroy { private translate: TranslateService ) {} + ngOnInit(): void { + this.expeditionService.refreshExpeditions(); + } + + ngAfterViewInit(): void { + this.updateSubscription = this.expeditionService + .getExpeditions() + .subscribe(() => this.calendarComponent.getApi().refetchEvents()); + } + + ngOnDestroy(): void { + this.updateSubscription?.unsubscribe(); + } + private handleEventClick(info: EventClickArg): void { if (info.event.extendedProps.type === CalendarEventType.EXPEDITION) { this.router.navigate(['expedition']); @@ -94,14 +108,4 @@ export class HomeCalendarComponent implements AfterViewInit, OnDestroy { } }; } - - ngAfterViewInit(): void { - this.updateSubscription = this.expeditionService - .getExpeditions() - .subscribe(() => this.calendarComponent.getApi().refetchEvents()); - } - - ngOnDestroy(): void { - this.updateSubscription?.unsubscribe(); - } } diff --git a/webapp/src/app/services/event/event.service.ts b/webapp/src/app/services/event/event.service.ts index c4d3bfb..da40987 100644 --- a/webapp/src/app/services/event/event.service.ts +++ b/webapp/src/app/services/event/event.service.ts @@ -1,8 +1,9 @@ import { Injectable, NgZone } from '@angular/core'; import { UserService } from '../user/user.service'; -import { Event, EventType } from '@nw-company-tool/model'; -import { Observable, Subject } from 'rxjs'; +import { Event, EventType, KeepAliveEvent } from '@nw-company-tool/model'; +import { interval, Observable, Subject, Subscription } from 'rxjs'; import { filter, map } from 'rxjs/operators'; +import * as moment from 'moment'; @Injectable({ providedIn: 'root' @@ -10,26 +11,71 @@ import { filter, map } from 'rxjs/operators'; export class EventService { private eventSource: EventSource; private events$ = new Subject(); + private lastKeepAlive: number; + private keepAliveSubscription: Subscription; + private reconnectSubscription: Subscription; - constructor(private zone: NgZone, private userService: UserService) {} - - init(): void { + constructor(private zone: NgZone, private userService: UserService) { this.userService.isLoggedIn$().subscribe((loggedIn) => { if (loggedIn) { - this.eventSource = new EventSource('/api/event', { withCredentials: true }); - this.eventSource.onmessage = (event) => { - this.zone.run(() => this.onEvent(JSON.parse(event.data))); - }; + this.connect(); } else { this.eventSource?.close(); } }); + this.subscribe(KeepAliveEvent).subscribe(() => this.onKeepAliveEvent()); + } + + private connect() { + this.eventSource = new EventSource('/api/event', { withCredentials: true }); + this.eventSource.onmessage = (event) => { + this.zone.run(() => this.onEvent(JSON.parse(event.data))); + }; + this.eventSource.onerror = () => { + this.disconnect(); + this.reconnect(); + }; + this.eventSource.onopen = () => { + this.lastKeepAlive = moment.now(); + console.info('connected to server'); + this.reconnectSubscription?.unsubscribe(); + this.reconnectSubscription = undefined; + this.keepAliveSubscription = interval(1000).subscribe(() => this.checkKeepAlive()); + }; + } + + private disconnect() { + this.keepAliveSubscription?.unsubscribe(); + this.eventSource?.close(); + } + + private reconnect() { + if (this.reconnectSubscription) { + // already trying to reconnect... + return; + } + this.reconnectSubscription = interval(2000).subscribe(() => { + console.log('reconnecting...'); + this.connect(); + }); } - private onEvent(event: Event) { + private onEvent(event: Event): void { this.events$.next(event); } + private onKeepAliveEvent(): void { + this.lastKeepAlive = moment.now(); + } + + private checkKeepAlive() { + if (moment.now() - this.lastKeepAlive > 20000) { + console.error('connection seems dead, trying to reconnect...'); + this.disconnect(); + this.reconnect(); + } + } + subscribe(eventType: EventType): Observable { const eventId = new eventType().id; return this.events$.pipe( diff --git a/webapp/src/app/services/expedition/expedition.service.ts b/webapp/src/app/services/expedition/expedition.service.ts index f1425b6..d62e48b 100644 --- a/webapp/src/app/services/expedition/expedition.service.ts +++ b/webapp/src/app/services/expedition/expedition.service.ts @@ -27,7 +27,6 @@ export class ExpeditionService { this.eventService.subscribe(ExpeditionDeleteEvent).subscribe((event) => this.onExpeditionDelete(event)); this.eventService.subscribe(ExpeditionJoinEvent).subscribe((event) => this.onExpeditionJoin(event)); this.eventService.subscribe(ExpeditionLeaveEvent).subscribe((event) => this.onExpeditionLeave(event)); - this.refreshExpeditions(); } public getExpeditions(): Observable { From 1da46972df3153929dcd29fd69608b04d11f4dfb Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Sat, 4 Dec 2021 07:44:41 +0100 Subject: [PATCH 21/25] feat: enhanced server update --- libs/model/src/admin.model.ts | 11 ++++ server/package-lock.json | 50 +++++++++++++++++-- server/package.json | 2 + server/src/admin/admin.controller.ts | 4 +- server/src/admin/admin.module.ts | 5 +- server/src/admin/admin.service.ts | 50 ++++++++++++++----- server/src/app.module.ts | 10 +++- .../src/middleware/maintenance.middleware.ts | 16 ++++++ .../src/app/interceptor/interceptor.module.ts | 4 +- ...interceptor.ts => response.interceptor.ts} | 5 +- .../src/app/pages/admin/admin-page.module.ts | 4 +- .../components/update/update.component.html | 6 +++ .../components/update/update.component.ts | 35 ++++++++++--- 13 files changed, 172 insertions(+), 30 deletions(-) create mode 100644 server/src/middleware/maintenance.middleware.ts rename webapp/src/app/interceptor/{token.interceptor.ts => response.interceptor.ts} (87%) diff --git a/libs/model/src/admin.model.ts b/libs/model/src/admin.model.ts index a06d604..51a009f 100644 --- a/libs/model/src/admin.model.ts +++ b/libs/model/src/admin.model.ts @@ -1,3 +1,5 @@ +import { Event } from './event.model'; + export type Version = { version: string; }; @@ -15,3 +17,12 @@ export interface EnableUser { export interface DeleteUser { userId: number; } + +export class ServerRestartEvent implements Event { + id = 'SERVER.RESTART'; +} + +export class ServerUpdateEvent implements Event { + id = 'SERVER.UPDATED'; + constructor(public message: string) {} +} diff --git a/server/package-lock.json b/server/package-lock.json index 22eb302..b807818 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/platform-express": "^8.0.0", "@nestjs/serve-static": "^2.2.2", "@prisma/client": "^3.5.0", + "cache-manager": "^3.6.0", "class-transformer": "^0.4.0", "class-validator": "^0.13.1", "cookie-parser": "^1.4.5", @@ -35,6 +36,7 @@ "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", "@types/better-sqlite3": "^7.4.1", + "@types/cache-manager": "^3.4.2", "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.13", "@types/jest": "^27.0.1", @@ -1849,6 +1851,12 @@ "@types/node": "*" } }, + "node_modules/@types/cache-manager": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-3.4.2.tgz", + "integrity": "sha512-1IwA74t5ID4KWo0Kndal16MhiPSZgMe1fGc+MLT6j5r+Ab7jku36PFTl4PP6MiWw0BJscM9QpZEo00qixNQoRg==", + "dev": true + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -2616,6 +2624,11 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3012,6 +3025,16 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.6.0.tgz", + "integrity": "sha512-D4GJZhyYgprYM30ZEPOn9kkdwdPUumX3ujbNbl7FYjcRViRvAgY53k6pO/82wNsm7c4aHVgXfR12/3huA47qnA==", + "dependencies": { + "async": "3.2.0", + "lodash": "^4.17.21", + "lru-cache": "6.0.0" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -6407,8 +6430,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.includes": { "version": "4.3.0", @@ -10561,6 +10583,12 @@ "@types/node": "*" } }, + "@types/cache-manager": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-3.4.2.tgz", + "integrity": "sha512-1IwA74t5ID4KWo0Kndal16MhiPSZgMe1fGc+MLT6j5r+Ab7jku36PFTl4PP6MiWw0BJscM9QpZEo00qixNQoRg==", + "dev": true + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -11191,6 +11219,11 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -11486,6 +11519,16 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" }, + "cache-manager": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.6.0.tgz", + "integrity": "sha512-D4GJZhyYgprYM30ZEPOn9kkdwdPUumX3ujbNbl7FYjcRViRvAgY53k6pO/82wNsm7c4aHVgXfR12/3huA47qnA==", + "requires": { + "async": "3.2.0", + "lodash": "^4.17.21", + "lru-cache": "6.0.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -14091,8 +14134,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.includes": { "version": "4.3.0", diff --git a/server/package.json b/server/package.json index 7ef9f41..78f950d 100644 --- a/server/package.json +++ b/server/package.json @@ -41,6 +41,7 @@ "@nestjs/platform-express": "^8.0.0", "@nestjs/serve-static": "^2.2.2", "@prisma/client": "^3.5.0", + "cache-manager": "^3.6.0", "class-transformer": "^0.4.0", "class-validator": "^0.13.1", "cookie-parser": "^1.4.5", @@ -60,6 +61,7 @@ "@nestjs/schematics": "^8.0.0", "@nestjs/testing": "^8.0.0", "@types/better-sqlite3": "^7.4.1", + "@types/cache-manager": "^3.4.2", "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.13", "@types/jest": "^27.0.1", diff --git a/server/src/admin/admin.controller.ts b/server/src/admin/admin.controller.ts index 35aebd4..df2f97e 100644 --- a/server/src/admin/admin.controller.ts +++ b/server/src/admin/admin.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Post } from '@nestjs/common'; +import { Body, CacheInterceptor, CacheTTL, Controller, Get, Post, UseInterceptors } from '@nestjs/common'; import { UserService } from '../user/user.service'; import { AdminService } from './admin.service'; import { Public, RequiredPermissions } from '../login/login.decorator'; @@ -9,6 +9,7 @@ import { DeleteUserDto } from './dto/user.delete.dto'; @Controller('/api/admin') @RequiredPermissions(Permission.ADMIN) +@UseInterceptors(CacheInterceptor) export class AdminController { constructor(private userService: UserService, private adminService: AdminService) {} @@ -51,6 +52,7 @@ export class AdminController { } @Get('/server/release/latest') + @CacheTTL(300) async getLatestRelease(): Promise { return this.adminService.getLatestReleaseVersion(); } diff --git a/server/src/admin/admin.module.ts b/server/src/admin/admin.module.ts index eb14638..a0c58ce 100644 --- a/server/src/admin/admin.module.ts +++ b/server/src/admin/admin.module.ts @@ -4,11 +4,12 @@ import { AdminService } from './admin.service'; import { UserModule } from '../user/user.module'; import { GithubModule } from '../github/github.module'; import { ArgsModule } from '../args/args.module'; +import { EventModule } from '../event/event.module'; @Module({ - imports: [UserModule, GithubModule, ArgsModule], + imports: [UserModule, GithubModule, ArgsModule, EventModule], controllers: [AdminController], providers: [AdminService], - exports: [], + exports: [AdminService], }) export class AdminModule {} diff --git a/server/src/admin/admin.service.ts b/server/src/admin/admin.service.ts index 019700d..62035d3 100644 --- a/server/src/admin/admin.service.ts +++ b/server/src/admin/admin.service.ts @@ -1,4 +1,4 @@ -import { HttpException, Injectable } from '@nestjs/common'; +import { HttpException, Injectable, Logger } from '@nestjs/common'; import process from 'process'; import * as fs from 'fs-extra'; import { GithubService } from '../github/github.service'; @@ -6,16 +6,27 @@ import { ArgsService, Flags } from '../args/args.service'; import { Readable } from 'stream'; import zlib from 'zlib'; import tar from 'tar-stream'; -import { GithubRelease, Version } from '@nw-company-tool/model'; +import { GithubRelease, ServerRestartEvent, ServerUpdateEvent, Version } from '@nw-company-tool/model'; +import { EventService } from '../event/event.service'; + +const logger = new Logger('NWCT Server'); @Injectable() export class AdminService { - constructor(private githubService: GithubService, private argsService: ArgsService) {} + private maintenance = false; + + constructor( + private githubService: GithubService, + private argsService: ArgsService, + private eventService: EventService, + ) {} public restart(): void { if (!process.send) { throw new HttpException('can not restart: this process seems to be no node child_process.', 500); } + this.log('restarting server....'); + this.eventService.emit(new ServerRestartEvent()); setTimeout(() => { process.send?.('restart'); }, 1000); @@ -27,6 +38,10 @@ export class AdminService { } public async getLatestReleaseVersion(): Promise { + if (this.argsService.getFlag(Flags.DEVELOPMENT)) { + logger.log('test'); + return { version: 'DEVELOPMENT' }; + } const githubRelease = this.argsService.getFlag(Flags.BETA) ? await this.githubService.getLatestBetaRelease() : await this.githubService.getLatestRelease(); @@ -41,29 +56,35 @@ export class AdminService { : await this.githubService.getLatestRelease(); } + public isMaintenance(): boolean { + return this.maintenance; + } + public async update(): Promise { if (this.argsService.getFlag(Flags.DEVELOPMENT)) { - console.log('server is running in development mode, skipping update.'); + this.log('server is running in development mode, skipping update.'); + this.restart(); return; } - console.log('starting update...'); + this.log('starting update...'); + this.maintenance = true; const latestRelease = await this.getLatestRelease(); const currentReleaseVersion = `v${this.getCurrentReleaseVersion().version}`; // package.json has release version without v if (latestRelease.name === currentReleaseVersion) { - console.log('already up to date.'); + this.log('already up to date.'); return; } - console.log(`latest release is ${latestRelease.name}`); + this.log(`latest release is ${latestRelease.name}`); const releaseAsset = latestRelease.assets.filter((asset) => asset.label === 'node distribution')[0]; if (!releaseAsset) { const errorMessage = 'can not update, latest release does not contain a node distribution asset.'; - console.error(errorMessage); + logger.error(errorMessage); throw new HttpException(errorMessage, 500); } const release = await this.githubService.downloadAsset(releaseAsset.browser_download_url); - console.log('download complete.'); + this.log('download complete.'); await this.performUpdate(release); - console.log(`update to ${latestRelease.name} complete.`); + this.log(`update to ${latestRelease.name} complete.`); this.restart(); } @@ -83,7 +104,7 @@ export class AdminService { if (header.type === 'file') { const filePath = `${process.cwd()}/${header.name}`; fs.outputFileSync(filePath, Buffer.concat(data)); - console.log(`updated: ${header.name}`); + this.log(`updated: ${header.name}`); } next(); }); @@ -91,11 +112,16 @@ export class AdminService { }); extract.on('finish', () => { - console.log('updating files complete.'); + this.log('updating files complete.'); resolve(); }); updateData.pipe(zlib.createGunzip()).pipe(extract); }); } + + private log(message: string): void { + logger.log(message); + this.eventService.emit(new ServerUpdateEvent(message)); + } } diff --git a/server/src/app.module.ts b/server/src/app.module.ts index a7fd49f..7960529 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,4 +1,4 @@ -import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; +import { CacheModule, MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; import { ConfigModule } from './config/config.module'; import { ArgsModule } from './args/args.module'; import { UserModule } from './user/user.module'; @@ -12,12 +12,17 @@ import { FrontendMiddleware } from './middleware/frontend.middleware'; import { ExpeditionModule } from './expedition/expedition.module'; import { EventModule } from './event/event.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; +import { MaintenanceMiddleware } from './middleware/maintenance.middleware'; @Module({ imports: [ EventEmitterModule.forRoot({ wildcard: true, }), + CacheModule.register({ + isGlobal: true, + ttl: 0, + }), TokenModule, ConfigModule, ArgsModule, @@ -34,5 +39,8 @@ import { EventEmitterModule } from '@nestjs/event-emitter'; export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer): any { consumer.apply(FrontendMiddleware).forRoutes({ path: '/**', method: RequestMethod.ALL }); + consumer + .apply(MaintenanceMiddleware) + .forRoutes({ path: '/api/**', method: RequestMethod.ALL }, { path: '/plugin/**', method: RequestMethod.ALL }); } } diff --git a/server/src/middleware/maintenance.middleware.ts b/server/src/middleware/maintenance.middleware.ts new file mode 100644 index 0000000..8476ec3 --- /dev/null +++ b/server/src/middleware/maintenance.middleware.ts @@ -0,0 +1,16 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { AdminService } from '../admin/admin.service'; +import { Request, Response } from 'express'; + +@Injectable() +export class MaintenanceMiddleware implements NestMiddleware { + constructor(private adminService: AdminService) {} + + use(req: Request, res: Response, next: () => void): any { + if (this.adminService.isMaintenance()) { + res.status(503).send({ message: 'server is under maintenance' }); + return; + } + next(); + } +} diff --git a/webapp/src/app/interceptor/interceptor.module.ts b/webapp/src/app/interceptor/interceptor.module.ts index 5063f55..7ac9566 100644 --- a/webapp/src/app/interceptor/interceptor.module.ts +++ b/webapp/src/app/interceptor/interceptor.module.ts @@ -1,12 +1,12 @@ import { NgModule } from '@angular/core'; import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; -import { TokenInterceptor } from './token.interceptor'; +import { ResponseInterceptor } from './response.interceptor'; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, - useClass: TokenInterceptor, + useClass: ResponseInterceptor, multi: true } ], diff --git a/webapp/src/app/interceptor/token.interceptor.ts b/webapp/src/app/interceptor/response.interceptor.ts similarity index 87% rename from webapp/src/app/interceptor/token.interceptor.ts rename to webapp/src/app/interceptor/response.interceptor.ts index d23650f..399d323 100644 --- a/webapp/src/app/interceptor/token.interceptor.ts +++ b/webapp/src/app/interceptor/response.interceptor.ts @@ -7,7 +7,7 @@ import { Router } from '@angular/router'; import { SnackbarService } from '../services/snackbar/snackbar.service'; @Injectable() -export class TokenInterceptor implements HttpInterceptor { +export class ResponseInterceptor implements HttpInterceptor { constructor(private userService: UserService, private router: Router, private snackbarService: SnackbarService) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { @@ -24,6 +24,9 @@ export class TokenInterceptor implements HttpInterceptor { this.router.navigate(['forbidden']); this.userService.refreshUser(); } + if (err.status === 503) { + this.snackbarService.error(err.error.message, 0); + } } throw throwError(err); }) diff --git a/webapp/src/app/pages/admin/admin-page.module.ts b/webapp/src/app/pages/admin/admin-page.module.ts index 44b9bcc..716d972 100644 --- a/webapp/src/app/pages/admin/admin-page.module.ts +++ b/webapp/src/app/pages/admin/admin-page.module.ts @@ -15,6 +15,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { CharacterDeleteComponent } from './components/character-delete/character-delete.component'; import { MatDialogModule } from '@angular/material/dialog'; +import { ReactiveFormsModule } from '@angular/forms'; @NgModule({ imports: [ @@ -29,7 +30,8 @@ import { MatDialogModule } from '@angular/material/dialog'; CommonModule, MatIconModule, MatTooltipModule, - MatDialogModule + MatDialogModule, + ReactiveFormsModule ], declarations: [AdminComponent, UsersTableComponent, UpdateComponent, CharacterDeleteComponent], providers: [], diff --git a/webapp/src/app/pages/admin/components/update/update.component.html b/webapp/src/app/pages/admin/components/update/update.component.html index 402d411..b09b442 100644 --- a/webapp/src/app/pages/admin/components/update/update.component.html +++ b/webapp/src/app/pages/admin/components/update/update.component.html @@ -3,4 +3,10 @@

Update

Current Release: {{currentVersion}}

Latest Release: {{latestVersion}}

+
+ + Log + + +
diff --git a/webapp/src/app/pages/admin/components/update/update.component.ts b/webapp/src/app/pages/admin/components/update/update.component.ts index 4ce76e0..69abfe1 100644 --- a/webapp/src/app/pages/admin/components/update/update.component.ts +++ b/webapp/src/app/pages/admin/components/update/update.component.ts @@ -1,17 +1,30 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { AdminService } from '../../../../services/admin/admin.service'; +import { EventService } from '../../../../services/event/event.service'; +import { Subscription } from 'rxjs'; +import { ServerRestartEvent, ServerUpdateEvent } from '@nw-company-tool/model'; +import { FormControl } from '@angular/forms'; +import { SnackbarService } from '../../../../services/snackbar/snackbar.service'; @Component({ selector: 'app-update', templateUrl: './update.component.html', styleUrls: ['./update.component.css'] }) -export class UpdateComponent implements OnInit { +export class UpdateComponent implements OnInit, OnDestroy { currentVersion: string; latestVersion: string; updateAvailable: boolean; - constructor(private adminService: AdminService) {} + private serverUpdateSubscription: Subscription; + private serverRestartSubscription: Subscription; + logForm = new FormControl(''); + + constructor( + private adminService: AdminService, + private eventService: EventService, + private snackbarService: SnackbarService + ) {} ngOnInit(): void { this.adminService @@ -21,11 +34,21 @@ export class UpdateComponent implements OnInit { .getLatestReleaseVersion() .subscribe((latestVersion) => (this.latestVersion = latestVersion.version)); this.adminService.isUpdateAvailable().subscribe((value) => (this.updateAvailable = value)); + this.serverUpdateSubscription = this.eventService.subscribe(ServerUpdateEvent).subscribe((event) => { + this.logForm.setValue(`${this.logForm.value}\n${event.message}`); + }); + this.serverRestartSubscription = this.eventService.subscribe(ServerRestartEvent).subscribe(() => { + this.snackbarService.open('server is restarting.. please stand by', 0); + setTimeout(() => window.location.reload(), 15000); + }); } update(): void { - this.adminService.update().subscribe(() => { - window.location.reload(); - }); + this.adminService.update().subscribe(); + } + + ngOnDestroy(): void { + this.serverUpdateSubscription.unsubscribe(); + this.serverRestartSubscription.unsubscribe(); } } From 189f35430a7eaf9e08e55550856ad511843952b8 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Sat, 4 Dec 2021 12:25:16 +0100 Subject: [PATCH 22/25] feat: expedition delete confirm dialog --- webapp/src/app/app.module.ts | 4 ++- .../confirm-dialog.component.css | 0 .../confirm-dialog.component.html | 6 ++++ .../confirm-dialog.component.spec.ts | 25 ++++++++++++++ .../confirm-dialog.component.ts | 29 ++++++++++++++++ .../confirm-dialog/confirm-dialog.module.ts | 13 +++++++ .../confirm-dialog/confirm-dialog.service.ts | 27 +++++++++++++++ .../expedition-table.component.ts | 34 +++++++++++++++++-- 8 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 webapp/src/app/components/confirm-dialog/confirm-dialog.component.css create mode 100644 webapp/src/app/components/confirm-dialog/confirm-dialog.component.html create mode 100644 webapp/src/app/components/confirm-dialog/confirm-dialog.component.spec.ts create mode 100644 webapp/src/app/components/confirm-dialog/confirm-dialog.component.ts create mode 100644 webapp/src/app/components/confirm-dialog/confirm-dialog.module.ts create mode 100644 webapp/src/app/components/confirm-dialog/confirm-dialog.service.ts diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts index 5b008a0..8886a0c 100644 --- a/webapp/src/app/app.module.ts +++ b/webapp/src/app/app.module.ts @@ -29,6 +29,7 @@ import { SnackbarModule } from './services/snackbar/snackbar.module'; import { HeaderModule } from './components/header/header.module'; import { FooterModule } from './components/footer/footer.module'; import { EventService } from './services/event/event.service'; +import { ConfirmDialogModule } from './components/confirm-dialog/confirm-dialog.module'; const cookieConfig: NgcCookieConsentConfig = { cookie: { @@ -85,7 +86,8 @@ FullCalendarModule.registerPlugins([dayGridPlugin, timeGridPlugin, interactionPl UserModule, // Global Components HeaderModule, - FooterModule + FooterModule, + ConfirmDialogModule ], providers: [{ provide: LOCALE_ID, useValue: 'en-GB' }, AppComponent, LoginGuard, AdminGuard], bootstrap: [AppComponent], diff --git a/webapp/src/app/components/confirm-dialog/confirm-dialog.component.css b/webapp/src/app/components/confirm-dialog/confirm-dialog.component.css new file mode 100644 index 0000000..e69de29 diff --git a/webapp/src/app/components/confirm-dialog/confirm-dialog.component.html b/webapp/src/app/components/confirm-dialog/confirm-dialog.component.html new file mode 100644 index 0000000..b3991e7 --- /dev/null +++ b/webapp/src/app/components/confirm-dialog/confirm-dialog.component.html @@ -0,0 +1,6 @@ +

{{data.title}}

+
{{data.content}}
+
+ + +
diff --git a/webapp/src/app/components/confirm-dialog/confirm-dialog.component.spec.ts b/webapp/src/app/components/confirm-dialog/confirm-dialog.component.spec.ts new file mode 100644 index 0000000..fe08dc5 --- /dev/null +++ b/webapp/src/app/components/confirm-dialog/confirm-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfirmDialogComponent } from './confirm-dialog.component'; + +describe('ConfirmDialogComponent', () => { + let component: ConfirmDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ConfirmDialogComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConfirmDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/webapp/src/app/components/confirm-dialog/confirm-dialog.component.ts b/webapp/src/app/components/confirm-dialog/confirm-dialog.component.ts new file mode 100644 index 0000000..58b91c5 --- /dev/null +++ b/webapp/src/app/components/confirm-dialog/confirm-dialog.component.ts @@ -0,0 +1,29 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { ConfirmDialogData, ConfirmDialogResult } from './confirm-dialog.service'; + +@Component({ + selector: 'app-confirm-dialog', + templateUrl: './confirm-dialog.component.html', + styleUrls: ['./confirm-dialog.component.css'] +}) +export class ConfirmDialogComponent { + constructor( + @Inject(MAT_DIALOG_DATA) public data: ConfirmDialogData, + private dialogRef: MatDialogRef + ) {} + + abort() { + const result: ConfirmDialogResult = { + confirmed: false + }; + this.dialogRef.close(result); + } + + confirm() { + const result: ConfirmDialogResult = { + confirmed: true + }; + this.dialogRef.close(result); + } +} diff --git a/webapp/src/app/components/confirm-dialog/confirm-dialog.module.ts b/webapp/src/app/components/confirm-dialog/confirm-dialog.module.ts new file mode 100644 index 0000000..ac9ad87 --- /dev/null +++ b/webapp/src/app/components/confirm-dialog/confirm-dialog.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { ConfirmDialog } from './confirm-dialog.service'; +import { ConfirmDialogComponent } from './confirm-dialog.component'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; + +@NgModule({ + imports: [MatDialogModule, MatButtonModule], + declarations: [ConfirmDialogComponent], + providers: [ConfirmDialog], + exports: [] +}) +export class ConfirmDialogModule {} diff --git a/webapp/src/app/components/confirm-dialog/confirm-dialog.service.ts b/webapp/src/app/components/confirm-dialog/confirm-dialog.service.ts new file mode 100644 index 0000000..036a147 --- /dev/null +++ b/webapp/src/app/components/confirm-dialog/confirm-dialog.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Observable } from 'rxjs'; +import { ConfirmDialogComponent } from './confirm-dialog.component'; + +export type ConfirmDialogData = { + title: string; + content: string; + confirmLabel: string; + abortLabel: string; +}; + +export type ConfirmDialogResult = { + confirmed: boolean; +}; + +@Injectable({ + providedIn: 'root' +}) +export class ConfirmDialog { + constructor(private dialog: MatDialog) {} + + open(data: ConfirmDialogData): Observable { + const dialogRef = this.dialog.open(ConfirmDialogComponent, { data }); + return dialogRef.afterClosed(); + } +} diff --git a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts index eba3392..233156d 100644 --- a/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts +++ b/webapp/src/app/pages/expedition/components/expedition-table/expedition-table.component.ts @@ -12,9 +12,11 @@ import { } from '../../../../components/character-detail/character-detail.component'; import { UserService } from '../../../../services/user/user.service'; import { Observable, of } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { first, map, mergeMap } from 'rxjs/operators'; import { ExpeditionJoinComponent, ExpeditionJoinDialogData } from '../expedition-join/expedition-join.component'; import { SnackbarService } from '../../../../services/snackbar/snackbar.service'; +import { ConfirmDialog } from '../../../../components/confirm-dialog/confirm-dialog.service'; +import { TranslateService } from '@ngx-translate/core'; export type Slot = { open: boolean; @@ -37,7 +39,9 @@ export class ExpeditionTableComponent implements OnInit, AfterViewInit { private expeditionService: ExpeditionService, private dialog: MatDialog, private userService: UserService, - private snackbarService: SnackbarService + private snackbarService: SnackbarService, + private confirmDialog: ConfirmDialog, + private translateService: TranslateService ) {} ngOnInit(): void { @@ -59,6 +63,13 @@ export class ExpeditionTableComponent implements OnInit, AfterViewInit { return moment.utc(date).local().format('YYYY-MM-DDTHH:mm:00'); } + formatDateLocal(date: string): string { + return moment + .utc(date) + .local() + .format(`${moment.localeData().longDateFormat('LL')} ${moment.localeData().longDateFormat('LT')}`); + } + countTuningOrbs(expedition: Expedition): number { return expedition.participants.reduce( (previousValue, currentValue) => previousValue + +currentValue.hasTuningOrb, @@ -167,7 +178,24 @@ export class ExpeditionTableComponent implements OnInit, AfterViewInit { } delete(expedition: Expedition) { - this.expeditionService.deleteExpedition(expedition.id); + this.translateService + .get(`EXPEDITION.${expedition.name}`) + .pipe( + mergeMap((name) => { + return this.confirmDialog.open({ + title: 'Delete Expedition', + content: `${name} - ${expedition.owner.characterName} - ${this.formatDateLocal(expedition.beginDateTime)}`, + abortLabel: 'Abort', + confirmLabel: 'Delete' + }); + }) + ) + + .subscribe((result) => { + if (result.confirmed) { + this.expeditionService.deleteExpedition(expedition.id); + } + }); } isOwner(expedition: Expedition): Observable { From 9817d3d0e6ca34686db7ba1cd25aef7af67dedcc Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Mon, 6 Dec 2021 07:35:38 +0100 Subject: [PATCH 23/25] fix: newest release was not considered larger than a beta release --- server/package-lock.json | 67 +++++++++++++++++++ server/package.json | 11 +-- server/src/github/github.service.ts | 8 +-- server/test/app.e2e-spec.ts | 9 +-- .../github/data/releases-newest-is-beta.json | 52 ++++++++++++++ server/test/github/data/releases.json | 42 ++++++++++++ server/test/github/github.service.spec.ts | 38 +++++++++++ server/test/jest.config-e2e.json | 17 +++++ .../test/{jest-e2e.json => jest.config.json} | 4 +- server/tsconfig.json | 1 + server/tsconfig.spec.json | 4 ++ 11 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 server/test/github/data/releases-newest-is-beta.json create mode 100644 server/test/github/data/releases.json create mode 100644 server/test/github/github.service.spec.ts create mode 100644 server/test/jest.config-e2e.json rename server/test/{jest-e2e.json => jest.config.json} (90%) create mode 100644 server/tsconfig.spec.json diff --git a/server/package-lock.json b/server/package-lock.json index b807818..276450a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -49,6 +49,7 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "jest": "^27.2.5", + "nock": "^13.2.1", "prettier": "^2.3.2", "source-map-support": "^0.5.20", "supertest": "^6.1.3", @@ -6284,6 +6285,12 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, "node_modules/json5": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", @@ -6479,6 +6486,12 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6758,6 +6771,21 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/nock": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.1.tgz", + "integrity": "sha512-CoHAabbqq/xZEknubuyQMjq6Lfi5b7RtK6SoNK6m40lebGp3yiMagWtIoYaw2s9sISD7wPuCfwFpivVHX/35RA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -7271,6 +7299,15 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -14016,6 +14053,12 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, "json5": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", @@ -14183,6 +14226,12 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -14394,6 +14443,18 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "nock": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.1.tgz", + "integrity": "sha512-CoHAabbqq/xZEknubuyQMjq6Lfi5b7RtK6SoNK6m40lebGp3yiMagWtIoYaw2s9sISD7wPuCfwFpivVHX/35RA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" + } + }, "node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -14773,6 +14834,12 @@ "sisteransi": "^1.0.5" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/server/package.json b/server/package.json index 78f950d..4235375 100644 --- a/server/package.json +++ b/server/package.json @@ -27,11 +27,11 @@ "start:dev": "nest start --watch -- --dataPath \"./../data/\" --development", "start:debug": "nest start --debug --watch -- --dataPath \"./../data/\" --development", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test": "jest --config test/jest.config.json", + "test:watch": "jest --config test/jest.config.json --watch", + "test:cov": "jest --config test/jest.config.json --coverage", + "test:debug": "node --config test/jest.config.json --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config test/jest.config-e2e.json" }, "dependencies": { "@nestjs/axios": "^0.0.3", @@ -74,6 +74,7 @@ "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", "jest": "^27.2.5", + "nock": "^13.2.1", "prettier": "^2.3.2", "source-map-support": "^0.5.20", "supertest": "^6.1.3", diff --git a/server/src/github/github.service.ts b/server/src/github/github.service.ts index ff920a7..dec25ad 100644 --- a/server/src/github/github.service.ts +++ b/server/src/github/github.service.ts @@ -25,11 +25,9 @@ export class GithubService { public async getLatestBetaRelease(): Promise { const releases = await this.getReleases(); - return releases - .filter((release) => release.prerelease) - .reduce((previousValue, currentValue) => - semver.gt(previousValue.name.substring(1), currentValue.name.substring(1)) ? previousValue : currentValue, - ); + return releases.reduce((previousValue, currentValue) => + semver.gt(previousValue.name.substring(1), currentValue.name.substring(1)) ? previousValue : currentValue, + ); } public getReleases(): Promise { diff --git a/server/test/app.e2e-spec.ts b/server/test/app.e2e-spec.ts index 50cda62..d4b4bab 100644 --- a/server/test/app.e2e-spec.ts +++ b/server/test/app.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from './../src/app.module'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; @@ -16,9 +16,6 @@ describe('AppController (e2e)', () => { }); it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); }); }); diff --git a/server/test/github/data/releases-newest-is-beta.json b/server/test/github/data/releases-newest-is-beta.json new file mode 100644 index 0000000..3504a2d --- /dev/null +++ b/server/test/github/data/releases-newest-is-beta.json @@ -0,0 +1,52 @@ +[ + { + "name": "v1.0.0-beta.1", + "prerelease": true, + "assets": [ + { + "label": "node distribution", + "browser_download_url": "some_url" + } + ] + }, + { + "name": "v1.0.0-beta.2", + "prerelease": true, + "assets": [ + { + "label": "node distribution", + "browser_download_url": "some_url" + } + ] + }, + { + "name": "v1.0.0-beta.3", + "prerelease": true, + "assets": [ + { + "label": "node distribution", + "browser_download_url": "some_url" + } + ] + }, + { + "name": "v1.0.0", + "prerelease": false, + "assets": [ + { + "label": "node distribution", + "browser_download_url": "some_url" + } + ] + }, + { + "name": "v1.1.0-beta.1", + "prerelease": false, + "assets": [ + { + "label": "node distribution", + "browser_download_url": "some_url" + } + ] + } +] diff --git a/server/test/github/data/releases.json b/server/test/github/data/releases.json new file mode 100644 index 0000000..c022928 --- /dev/null +++ b/server/test/github/data/releases.json @@ -0,0 +1,42 @@ +[ + { + "name": "v1.0.0-beta.1", + "prerelease": true, + "assets": [ + { + "label": "node distribution", + "browser_download_url": "some_url" + } + ] + }, + { + "name": "v1.0.0-beta.2", + "prerelease": true, + "assets": [ + { + "label": "node distribution", + "browser_download_url": "some_url" + } + ] + }, + { + "name": "v1.0.0-beta.3", + "prerelease": true, + "assets": [ + { + "label": "node distribution", + "browser_download_url": "some_url" + } + ] + }, + { + "name": "v1.0.0", + "prerelease": false, + "assets": [ + { + "label": "node distribution", + "browser_download_url": "some_url" + } + ] + } +] diff --git a/server/test/github/github.service.spec.ts b/server/test/github/github.service.spec.ts new file mode 100644 index 0000000..0cee177 --- /dev/null +++ b/server/test/github/github.service.spec.ts @@ -0,0 +1,38 @@ +import nock from 'nock'; + +import releases from './data/releases.json'; +import releasesBeta from './data/releases-newest-is-beta.json'; +import { GithubModule } from '../../src/github/github.module'; +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { GithubService } from '../../src/github/github.service'; + +describe('GitHub Service', () => { + let app: INestApplication; + + beforeEach(async () => { + nock.cleanAll(); + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [GithubModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('should return correct latest release version if there is no older beta release', async () => { + nock('https://api.github.com/').get('/repos/cbartel/nw-company-tool/releases').reply(200, JSON.stringify(releases)); + const githubService = app.get(GithubService); + const latestBetaRelease = await githubService.getLatestBetaRelease(); + expect(latestBetaRelease.name).toEqual('v1.0.0'); + }); + + it('should return correct latest beta release version', async () => { + nock('https://api.github.com/') + .get('/repos/cbartel/nw-company-tool/releases') + .reply(200, JSON.stringify(releasesBeta)); + const githubService = app.get(GithubService); + const latestBetaRelease = await githubService.getLatestBetaRelease(); + expect(latestBetaRelease.name).toEqual('v1.1.0-beta.1'); + }); +}); diff --git a/server/test/jest.config-e2e.json b/server/test/jest.config-e2e.json new file mode 100644 index 0000000..756a602 --- /dev/null +++ b/server/test/jest.config-e2e.json @@ -0,0 +1,17 @@ +{ + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": "\\.e2e-spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "moduleNameMapper": { + "@nw-company-tool/model/(.*)": "/../libs/model/src/$1", + "@nw-company-tool/model": "/../libs/model/src" + } +} diff --git a/server/test/jest-e2e.json b/server/test/jest.config.json similarity index 90% rename from server/test/jest-e2e.json rename to server/test/jest.config.json index c7f90e5..84c36c8 100644 --- a/server/test/jest-e2e.json +++ b/server/test/jest.config.json @@ -6,7 +6,7 @@ ], "rootDir": ".", "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", + "testRegex": "\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, @@ -14,4 +14,4 @@ "@nw-company-tool/model/(.*)": "/../libs/model/src/$1", "@nw-company-tool/model": "/../libs/model/src" } -} \ No newline at end of file +} diff --git a/server/tsconfig.json b/server/tsconfig.json index 6ea32f2..ec5a282 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -19,6 +19,7 @@ "outDir": "./dist", "baseUrl": "./", "esModuleInterop": true, + "resolveJsonModule": true, "incremental": true, "skipLibCheck": true, "strictNullChecks": false, diff --git a/server/tsconfig.spec.json b/server/tsconfig.spec.json new file mode 100644 index 0000000..0550442 --- /dev/null +++ b/server/tsconfig.spec.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": [ "test", "**/*spec.ts"] +} From a0e8d09f5d483d8d73bf8224cee9e111ef53fe2a Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Mon, 6 Dec 2021 08:30:59 +0100 Subject: [PATCH 24/25] docs: updated README.md --- README.md | 76 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 13e1663..61c0db9 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,37 @@ -# ⛏ New World Company Tool 🔨 - -[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/J3J37247V) - -Discord - -![GitHub Repo stars](https://img.shields.io/github/stars/cbartel/nw-company-tool?style=social) -![GitHub watchers](https://img.shields.io/github/watchers/cbartel/nw-company-tool?style=social) - -![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/cbartel/nw-company-tool?style=flat-square) -![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/cbartel/nw-company-tool?include_prereleases&label=beta&style=flat-square) -![GitHub Workflow Status](https://img.shields.io/github/workflow/status/cbartel/nw-company-tool/Release?style=flat-square) -![GitHub last commit (branch)](https://img.shields.io/github/last-commit/cbartel/nw-company-tool/develop?style=flat-square) -![GitHub all releases](https://img.shields.io/github/downloads/cbartel/nw-company-tool/total?style=flat-square) -![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/cbartel/nw-company-tool?style=flat-square) -## ❓ What is this ❓ - -This tool is intended to use with the game New World from Amazon Game Studios. It aims to give all your company members + + + + +

+ ⛏ New World Company Tool 🔨 +

+ +
+
+ Support me on Ko-fi +
+
+ Discord +
+
+ GitHub Repo stars + GitHub watchers +
+
+ GitHub release (latest SemVer) + GitHub release (latest SemVer including pre-releases) + GitHub Workflow Status (branch) + GitHub last commit + GitHub all releases + GitHub closed issues +
+
+ +

+ ❓ What is this ❓ +

+ +This tool is intended to be used with the game New World from Amazon Game Studios. It aims to give all your company members an overview about other members, their skills and attributes. The tool uses Discord's login and enables all your members to reach out to other company members easily. @@ -23,18 +39,19 @@ The server is written in [nest.js](https://nestjs.com/) and uses [prisma](https: [SQLite](https://www.sqlite.org/index.html). It runs on [nodejs](https://nodejs.dev/). The webapp is written in [angular](https://angular.io/). -The tool is currently in an early development state but already usable and stable. It provides your company members with -the ability to maintain their level, gear score, attributes, weapon- and trade skills and shows the data on a company table. - -### 🛣️ Roadmap 🛣️ +

+ 🛣️ Roadmap 🛣️ +

- develop a plugin system to enable other developers to create content for this tool - create a war planner to make it easier for your company to organize their war efforts -- create an expedition planner +- ~~create an expedition planner~~ ✅ (Release 2.1.0) - integrate [NWDB.info](https://nwdb.info/) or a similar website - enable users to create skill tree builds in this tool to share their builds with your company -### 📸 Footage 📸 +

+ 📸 Footage 📸 +

#### Company Overview ![Company Table](docs/img/company_table.png) @@ -42,7 +59,10 @@ the ability to maintain their level, gear score, attributes, weapon- and trade s ![My Character](docs/img/my_character.png) -## 🚀 How to install 🚀 +

+ 🚀 How to install 🚀 +

+ ### Download Make sure you have [nodejs](https://nodejs.dev/) installed! @@ -107,7 +127,7 @@ as you see, you need to set up a discord application with OAuth2, this is pretty - create a new application at https://discord.com/developers/applications - navigate to OAuth2, there you will find your `CLIENT_ID` and `CLIENT_SECRET` - edit your redirects, in this example your NWCT server uses `http://www.example.com:8080` as `BASE_URL`, the redirect would -then be `http://www.example.com:8080/api/login/callback`. Be careful to not any unwanted forward slashes or your login will not work properly. +then be `http://www.example.com:8080/api/login/callback`. Be careful not to add any unwanted forward slashes or your login will not work properly. Go back to your `config.json` file, insert the needed values and save this file. @@ -122,7 +142,7 @@ node dist/main.js --dataPath "/opt/nwct/nwct-data/" ``` ### PM2, nginx and SSL -I would highly recommend the run NWCT via [pm2](https://pm2.keymetrics.io/) behind a +I would highly recommend to run NWCT via [pm2](https://pm2.keymetrics.io/) behind a [nginx proxy server](https://www.nginx.com/) using [certbot](https://certbot.eff.org/instructions). Reach out for the documentation of pm2 and nginx on how to install it on your machine. @@ -189,8 +209,6 @@ server { # Enter your fully qualified domain name or leave blank server_name www.example.com; - # Listen on port 80 without SSL certificates - # Sets the Max Upload size to 300 MB client_max_body_size 300M; From f973f36166c10e595d07a3e31a47a827cf8c6ab6 Mon Sep 17 00:00:00 2001 From: Christopher Bartel Date: Mon, 6 Dec 2021 12:38:50 +0100 Subject: [PATCH 25/25] feat: github button --- .../src/app/components/footer/footer.component.css | 6 ++++++ .../app/components/footer/footer.component.html | 14 ++++++++++++-- .../src/app/components/footer/footer.component.ts | 4 ++++ webapp/src/app/components/footer/footer.module.ts | 3 ++- webapp/src/styles.css | 4 ++++ 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/webapp/src/app/components/footer/footer.component.css b/webapp/src/app/components/footer/footer.component.css index c79af70..cdcd692 100644 --- a/webapp/src/app/components/footer/footer.component.css +++ b/webapp/src/app/components/footer/footer.component.css @@ -6,3 +6,9 @@ footer { padding: 1rem; text-align: center; } + +.github-icon { + display: flex; + flex-grow: 1; + justify-content: right; +} diff --git a/webapp/src/app/components/footer/footer.component.html b/webapp/src/app/components/footer/footer.component.html index 34e113e..57d37f1 100644 --- a/webapp/src/app/components/footer/footer.component.html +++ b/webapp/src/app/components/footer/footer.component.html @@ -1,3 +1,13 @@ -
-

New World Company Tool {{version}} 🔨 crafted with ❤️ by Krise

+
+

New World Company Tool {{version}} + 🔨 crafted with ❤️ by Krise

+ +
+ +
diff --git a/webapp/src/app/components/footer/footer.component.ts b/webapp/src/app/components/footer/footer.component.ts index b321d26..42cb045 100644 --- a/webapp/src/app/components/footer/footer.component.ts +++ b/webapp/src/app/components/footer/footer.component.ts @@ -14,4 +14,8 @@ export class FooterComponent implements OnInit { ngOnInit(): void { this.configService.getVersion().subscribe((version) => (this.version = version.version)); } + + openGithub(): void { + window.open('https://github.com/cbartel/nw-company-tool', '_blank'); + } } diff --git a/webapp/src/app/components/footer/footer.module.ts b/webapp/src/app/components/footer/footer.module.ts index 50395dc..40c09d0 100644 --- a/webapp/src/app/components/footer/footer.module.ts +++ b/webapp/src/app/components/footer/footer.module.ts @@ -1,8 +1,9 @@ import { NgModule } from '@angular/core'; import { FooterComponent } from './footer.component'; +import { MatButtonModule } from '@angular/material/button'; @NgModule({ - imports: [], + imports: [MatButtonModule], declarations: [FooterComponent], exports: [FooterComponent] }) diff --git a/webapp/src/styles.css b/webapp/src/styles.css index a889a54..f424bbf 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -109,3 +109,7 @@ input[type=number] { .w-150 { max-width: 150px; } + +.margin-left-auto { + margin-left: auto; +}