diff --git a/angular.json b/angular.json index 4deb648..1875b03 100644 --- a/angular.json +++ b/angular.json @@ -28,6 +28,7 @@ "src/assets" ], "styles": [ + "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", "src/styles.scss" ], "scripts": [] @@ -92,6 +93,7 @@ "src/assets" ], "styles": [ + "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", "src/styles.scss" ], "scripts": [] @@ -123,6 +125,7 @@ } } } - }}, + } + }, "defaultProject": "ng-peti" -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 512f702..ebcda8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -199,6 +199,22 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-9.1.1.tgz", "integrity": "sha512-IvKv8sV0ymbzDEX2ZLW+F6nOTQqDYallHexuzRVT9txvNE8TNHyySvLcyC5dTmX9fj9LA72NZ6nFyhxq0LFvtQ==" }, + "@angular/cdk": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.4.tgz", + "integrity": "sha512-iw2+qHMXHYVC6K/fttHeNHIieSKiTEodVutZoOEcBu9rmRTGbLB26V/CRsfIRmA1RBk+uFYWc6UQZnMC3RdnJQ==", + "requires": { + "parse5": "^5.0.0" + }, + "dependencies": { + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "optional": true + } + } + }, "@angular/cli": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-9.1.1.tgz", @@ -470,6 +486,218 @@ "integrity": "sha512-T+/0X2VnmgW/vzynqYTVv29qtebNvrCB/yJqtNIlqXvBjcB8XRRwZPDZvRyl5BiwEPSsJnjdRFNH9krQHxYp+g==", "dev": true }, + "@angular/localize": { + "version": "9.1.12", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-9.1.12.tgz", + "integrity": "sha512-31OalfES+dLrxN0VXCxxtT5dWoOSlQ40KYmzMS8X+mQ20gy9eFiZK4qf3DEq3JPqRltBMdEDnwR38uGIMAu2gQ==", + "requires": { + "@babel/core": "7.8.3", + "glob": "7.1.2", + "yargs": "15.3.0" + }, + "dependencies": { + "@babel/core": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.3.tgz", + "integrity": "sha512-4XFkf8AwyrEG7Ziu3L2L0Cv+WyY47Tcsp70JFmpftbAA1K7YL/sgE9jh9HyNj08Y/U50ItUchpN0w6HxAoX1rA==", + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.3", + "@babel/helpers": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.0.tgz", + "integrity": "sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.0" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "@angular/material": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-9.2.4.tgz", + "integrity": "sha512-LkoTXE6B0slvMhvfZDdPWaz4yaYLkaAp5VSPunI9pxGsPxzqEV9e210wC1/sjG/76Nk8Ep7/2z9XKac8Q9bMwA==" + }, "@angular/platform-browser": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-9.1.1.tgz", @@ -489,7 +717,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, "requires": { "@babel/highlight": "^7.8.3" } @@ -555,7 +782,6 @@ "version": "7.9.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.3.tgz", "integrity": "sha512-RpxM252EYsz9qLUIq6F7YJyK1sv0wWDBFuztfDGWaQKzHjqDHysxSiRUpA/X9jmfqo+WzkAVKFaUily5h+gDCQ==", - "dev": true, "requires": { "@babel/types": "^7.9.0", "jsesc": "^2.5.1", @@ -566,8 +792,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -647,7 +872,6 @@ "version": "7.9.5", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz", "integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==", - "dev": true, "requires": { "@babel/helper-get-function-arity": "^7.8.3", "@babel/template": "^7.8.3", @@ -658,7 +882,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", - "dev": true, "requires": { "@babel/types": "^7.8.3" } @@ -768,7 +991,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", - "dev": true, "requires": { "@babel/types": "^7.8.3" } @@ -776,8 +998,7 @@ "@babel/helper-validator-identifier": { "version": "7.9.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", - "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", - "dev": true + "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==" }, "@babel/helper-wrap-function": { "version": "7.8.3", @@ -795,7 +1016,6 @@ "version": "7.9.2", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.9.2.tgz", "integrity": "sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==", - "dev": true, "requires": { "@babel/template": "^7.8.3", "@babel/traverse": "^7.9.0", @@ -806,7 +1026,6 @@ "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", @@ -816,8 +1035,7 @@ "@babel/parser": { "version": "7.9.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.4.tgz", - "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==", - "dev": true + "integrity": "sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==" }, "@babel/plugin-proposal-async-generator-functions": { "version": "7.8.3", @@ -1400,7 +1618,6 @@ "version": "7.8.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", - "dev": true, "requires": { "@babel/code-frame": "^7.8.3", "@babel/parser": "^7.8.6", @@ -1411,7 +1628,6 @@ "version": "7.9.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.5.tgz", "integrity": "sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ==", - "dev": true, "requires": { "@babel/code-frame": "^7.8.3", "@babel/generator": "^7.9.5", @@ -1428,7 +1644,6 @@ "version": "7.9.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.5.tgz", "integrity": "sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ==", - "dev": true, "requires": { "@babel/types": "^7.9.5", "jsesc": "^2.5.1", @@ -1439,8 +1654,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, @@ -1448,13 +1662,64 @@ "version": "7.9.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.5.tgz", "integrity": "sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.9.5", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } }, + "@fortawesome/angular-fontawesome": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.7.0.tgz", + "integrity": "sha512-U+eHYbKuVYrrm9SnIfl+z+6KTiI4Pu+S2OKh34JIi7C1jHhDcrVeDZISP/cpswHY7LWWDOPYeKE+yuWFlL4aVw==", + "requires": { + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, + "@fortawesome/fontawesome-common-types": { + "version": "0.2.30", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.30.tgz", + "integrity": "sha512-TsRwpTuKwFNiPhk1UfKgw7zNPeV5RhNp2Uw3pws+9gDAkPGKrtjR1y2lI3SYn7+YzyfuNknflpBA1LRKjt7hMg==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "1.2.30", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.30.tgz", + "integrity": "sha512-E3sAXATKCSVnT17HYmZjjbcmwihrNOCkoU7dVMlasrcwiJAHxSKeZ+4WN5O+ElgO/FaYgJmASl8p9N7/B/RttA==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.30" + } + }, + "@fortawesome/free-brands-svg-icons": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.14.0.tgz", + "integrity": "sha512-WsqPFTvJFI7MYkcy0jeFE2zY+blC4OrnB9MJOcn1NxRXT/sSfEEhrI7CwzIkiYajLiVDBKWeErYOvpsMeodmCQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.30" + } + }, + "@fortawesome/free-regular-svg-icons": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.14.0.tgz", + "integrity": "sha512-6LCFvjGSMPoUQbn3NVlgiG4CY5iIY8fOm+to/D6QS/GvdqhDt+xZklQeERdCvVRbnFa1ITc1rJHPRXqkX5wztQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.30" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.14.0.tgz", + "integrity": "sha512-M933RDM8cecaKMWDSk3FRYdnzWGW7kBBlGNGfvqLVwcwhUPNj9gcw+xZMrqBdRqxnSXdl3zWzTCNNGEtFUq67Q==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.30" + } + }, "@istanbuljs/schema": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", @@ -1496,6 +1761,11 @@ } } }, + "@ng-bootstrap/ng-bootstrap": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-6.2.0.tgz", + "integrity": "sha512-wqwhnJFyEwvzWQJjXrEt+7oBTSvu2qPbdYvUFYhDVzOJLWB5M7YEhDAkMrfHQJ0pZNBMGr580FqYue+XiURY0Q==" + }, "@ngtools/webpack": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-9.1.1.tgz", @@ -1560,8 +1830,7 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/events": { "version": "3.0.0", @@ -1965,7 +2234,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -2286,8 +2554,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -2492,11 +2759,15 @@ "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, + "bootstrap": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.2.tgz", + "integrity": "sha512-vlGn0bcySYl/iV+BGA544JkkZP5LB3jsmkeKLFQakCOwCM3AOk7VkldBz4jrzSe+Z0Ezn99NVXa1o45cQY4R6A==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2776,8 +3047,7 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caniuse-api": { "version": "3.0.0", @@ -2813,7 +3083,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -3051,7 +3320,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -3059,8 +3327,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { "version": "1.5.3", @@ -3167,8 +3434,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -3248,7 +3514,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -3795,7 +4060,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, "requires": { "ms": "^2.1.1" } @@ -3809,8 +4073,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decode-uri-component": { "version": "0.2.0", @@ -4191,8 +4454,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "emojis-list": { "version": "3.0.0", @@ -4439,8 +4701,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint-scope": { "version": "4.0.3", @@ -5086,8 +5347,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.1.2", @@ -5111,8 +5371,7 @@ "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", - "dev": true + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" }, "get-caller-file": { "version": "1.0.3", @@ -5182,8 +5441,7 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "globby": { "version": "7.1.1", @@ -5279,8 +5537,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.1", @@ -5685,7 +5942,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5694,8 +5950,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", @@ -6392,8 +6647,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.13.1", @@ -6414,8 +6668,7 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, "json-parse-better-errors": { "version": "1.0.2", @@ -6451,7 +6704,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, "requires": { "minimist": "^1.2.5" } @@ -6740,8 +6992,7 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.clonedeep": { "version": "4.5.0", @@ -7234,7 +7485,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7242,8 +7492,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { "version": "3.1.1", @@ -7367,8 +7616,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "multicast-dns": { "version": "6.2.3", @@ -7423,6 +7671,14 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, + "ngx-webstorage-service": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ngx-webstorage-service/-/ngx-webstorage-service-4.1.0.tgz", + "integrity": "sha512-t0aO4X8rqesLqypU8ZK2W7xGVbpi/z7u/Xg7qhnLoKVauI29NFzdKxV2oaYZmpkxIpgdbLSsIfklkjgxIM0IFA==", + "requires": { + "tslib": "^1.9.0" + } + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -7813,7 +8069,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -8326,8 +8581,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -8344,8 +8598,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "0.1.7", @@ -9751,8 +10004,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "1.0.1", @@ -9770,7 +10022,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -9893,8 +10144,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -10196,8 +10446,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-immediate-shim": { "version": "1.0.1", @@ -11196,7 +11445,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -11421,8 +11669,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", @@ -13690,8 +13937,7 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wordwrap": { "version": "0.0.3", @@ -13774,8 +14020,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { "version": "6.2.1", @@ -13817,8 +14062,7 @@ "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yallist": { "version": "4.0.0", diff --git a/package.json b/package.json index 72b4bd7..b05f3fc 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,24 @@ "private": true, "dependencies": { "@angular/animations": "~9.1.1", + "@angular/cdk": "^9.2.4", "@angular/common": "~9.1.1", "@angular/compiler": "~9.1.1", "@angular/core": "~9.1.1", "@angular/forms": "~9.1.1", + "@angular/localize": "~9.1.1", + "@angular/material": "^9.2.4", "@angular/platform-browser": "~9.1.1", "@angular/platform-browser-dynamic": "~9.1.1", "@angular/router": "~9.1.1", + "@fortawesome/angular-fontawesome": "^0.7.0", + "@fortawesome/fontawesome-svg-core": "^1.2.30", + "@fortawesome/free-brands-svg-icons": "^5.14.0", + "@fortawesome/free-regular-svg-icons": "^5.14.0", + "@fortawesome/free-solid-svg-icons": "^5.14.0", + "@ng-bootstrap/ng-bootstrap": "^6.2.0", + "bootstrap": "^4.5.2", + "ngx-webstorage-service": "^4.1.0", "rxjs": "~6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 06c7342..3dcfdc2 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,8 +1,23 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { HomePageComponent } from './home-page/home-page.component'; +import { SigninComponent } from './signin/signin.component'; +import { SignupComponent } from './signup/signup.component'; +import { ErrorPageComponent } from './error-page/error-page.component'; +import { ArticleFormComponent } from './article-form/article-form.component'; +import { ArticlePageComponent } from './article-page/article-page.component'; -const routes: Routes = []; +const routes: Routes = [ + {path: '', component: HomePageComponent}, + {path: 'home/:articlesMode/:page', component: HomePageComponent}, + {path: 'home', component: HomePageComponent}, + {path: 'login', component: SigninComponent}, + {path: 'register', component: SignupComponent}, + {path: 'editor/:mode', component: ArticleFormComponent}, + {path: 'article', component: ArticlePageComponent}, + {path: '**', component: ErrorPageComponent} +]; @NgModule({ imports: [RouterModule.forRoot(routes)], diff --git a/src/app/app.component.html b/src/app/app.component.html index e48d8c1..6de55ce 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,534 +1,81 @@ - - - - - - - - - - - - - - -
- - -
- - - - - - - - - - - - {{ title }} app is running! - - - - - -
- - -

Resources

-

Here are some links to help you get started:

- -
- - - - Learn Angular - - - - - - - CLI Documentation - - - - - - - - Angular Blog - - - - -
- - -

Next Steps

-

What do you want to do next with your app?

- - - -
-
- - - New Component +
+ +
+ + + -
- - Angular Material -
+
+ +
-
- - Add PWA Support -
+ + + + \ No newline at end of file diff --git a/src/app/app.component.scss b/src/app/app.component.scss index e69de29..46f7bf9 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -0,0 +1,28 @@ +.text-link { + color: #007adf !important; + } + +.text-link:hover, .text-link:active { + color: #008cff !important; +} + +.bg-navbar-custom{ + background-color: #8c00ff; +} + +.footer-copyright{ + background-color: rgb(32, 31, 31); +} + +.fill-screen{ + height:100%; +} + +.nav-link{ + color:lightgray !important; + font-weight: bold; +} + +.nav-link:hover{ + color: white !important; +} \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 6a08b74..cbdf104 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,10 +1,32 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, OnChanges, DoCheck } from '@angular/core'; +import { LocalStorageService } from './services/local-storage.service'; +import { Router } from '@angular/router'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) -export class AppComponent { +export class AppComponent implements OnInit, DoCheck { title = 'ng-peti'; + token = ''; + + constructor( + private storage: LocalStorageService, + private router: Router + ){} + + ngOnInit(){ + this.token = this.storage.getAuthentication(); + } + + ngDoCheck(){ + this.token = this.storage.getAuthentication(); // has delay in updating header, but update it without refresh page (routing) + } + + logOut(){ + this.storage.logOut(); + this.token = undefined; + this.router.navigate(['login']); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2c3ba29..7c38940 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -3,14 +3,49 @@ import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; +import { HomePageComponent } from './home-page/home-page.component'; +import { SignupComponent } from './signup/signup.component'; +import { SigninComponent } from './signin/signin.component'; +import { ErrorPageComponent } from './error-page/error-page.component'; +import { ArticlesListComponent } from './articles-list/articles-list.component'; +import { CommentsListComponent } from './comments-list/comments-list.component'; +import { CommentFormComponent } from './comment-form/comment-form.component'; +import { ArticleFormComponent } from './article-form/article-form.component'; +import { ArticlePageComponent } from './article-page/article-page.component'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; +import { StorageServiceModule} from 'ngx-webstorage-service'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatInputModule } from '@angular/material/input'; + @NgModule({ declarations: [ - AppComponent + AppComponent, + HomePageComponent, + SignupComponent, + SigninComponent, + ErrorPageComponent, + ArticlesListComponent, + CommentsListComponent, + CommentFormComponent, + ArticleFormComponent, + ArticlePageComponent ], imports: [ BrowserModule, - AppRoutingModule + AppRoutingModule, + NgbModule, + ReactiveFormsModule, + HttpClientModule, + StorageServiceModule, + FontAwesomeModule, + BrowserAnimationsModule, + MatAutocompleteModule, + MatInputModule ], providers: [], bootstrap: [AppComponent] diff --git a/src/app/article-form/article-form.component.html b/src/app/article-form/article-form.component.html new file mode 100644 index 0000000..f18ea70 --- /dev/null +++ b/src/app/article-form/article-form.component.html @@ -0,0 +1,43 @@ +
+

Article

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + {{selectedTag}} + x + +
+ + + + {{tag}} + + +
+ + +
+
\ No newline at end of file diff --git a/src/app/article-form/article-form.component.scss b/src/app/article-form/article-form.component.scss new file mode 100644 index 0000000..9aa623d --- /dev/null +++ b/src/app/article-form/article-form.component.scss @@ -0,0 +1,47 @@ +.center-form{ + width: 80%; + margin: 0 auto; + margin-top: 20px; + margin-bottom: 20px; + padding: 10px; + border-radius: 2px; + border: solid 1px gray; + box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.445); + background-color: white; +} + +.section-title{ + font-size: xx-large; + font-family: Arial, Helvetica, sans-serif; + font-weight: bold; + text-align: center; +} + +.article-body{ + min-height: 150px; +} + +.selected-tag{ + background-color: gray; + font-weight: bold; + color: white; + padding: 5px; + margin: 2px; + border-radius: 10px; + display: inline-block; +} + +.deselect-tag{ + color: white; +} + +.deselect-tag:hover{ + color: tomato; + text-decoration: none; +} + +.tag-form{ + width: 30%; + display: inline-block; + margin-right: 20px; +} \ No newline at end of file diff --git a/src/app/article-form/article-form.component.spec.ts b/src/app/article-form/article-form.component.spec.ts new file mode 100644 index 0000000..e90d9ac --- /dev/null +++ b/src/app/article-form/article-form.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ArticleFormComponent } from './article-form.component'; + +describe('ArticleFormComponent', () => { + let component: ArticleFormComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ArticleFormComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ArticleFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/article-form/article-form.component.ts b/src/app/article-form/article-form.component.ts new file mode 100644 index 0000000..ec00f3a --- /dev/null +++ b/src/app/article-form/article-form.component.ts @@ -0,0 +1,145 @@ +import { Component, OnInit, Input, OnDestroy } from '@angular/core'; +import { FormGroup, FormControl, Validators, AbstractControl, AsyncValidatorFn } from '@angular/forms'; +import { HttpService } from '../services/http.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ArticleService } from '../services/article.service'; +import { Article } from '../model/article'; +import { Observable, Subscription } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; + +@Component({ + selector: 'app-article-form', + templateUrl: './article-form.component.html', + styleUrls: ['./article-form.component.scss'] +}) +export class ArticleFormComponent implements OnInit, OnDestroy { + + tags: string[] = []; + filteredTags: Observable; + selectedTags: string[] = []; + + articleForm = new FormGroup({ + title: new FormControl('', [Validators.required]), + description: new FormControl('', [Validators.required]), + body: new FormControl('', [Validators.required]), + tagList: new FormControl('') + }, + [this.notEmptySelectedTags()] + ); + + mode: string; + article: Article; + + modeSubscription: Subscription; + + constructor( + private http: HttpService, + private route: ActivatedRoute, + private articleService: ArticleService, + private router: Router + ) { } + + notEmptySelectedTags(){ + return (control: AbstractControl) => { + if (this.selectedTags.length === 0){ + return {EmptySelectedTags: true}; + } + return null; + }; + } + + ngOnInit(): void { + this.modeSubscription = this.route.params.subscribe( params => this.mode = params.mode); + this.autocompleteTags(); + + switch (this.mode){ + case 'update': + this.articleForm.get('tagList').disable(); + this.article = this.articleService.getArticle(); + if (this.article !== undefined){ + this.articleForm.patchValue(this.article); + this.selectedTags = this.article.tagList; + } + break; + case 'create': + break; + default: + this.router.navigate(['error']); + } + + } + + saveArticle(){ + switch (this.mode){ + case 'update': + this.updateArticle(); + break; + case 'create': + this.createArticle(); + break; + } + } + + ngOnDestroy(){ + this.modeSubscription.unsubscribe(); + } + + createArticle(){ + const article: Article = this.articleForm.value; + article.tagList = this.selectedTags; + + this.http.createArticle(article).subscribe(response => { + if (response.errors !== undefined){ + alert('Error when creating article'); + } + else{ + alert('Article successfully created'); + this.router.navigate(['home/myArticles/1']); + } + }); + } + + updateArticle(){ + Object.assign(this.article, this.articleForm.value); + this.article.tagList = this.selectedTags; + + this.http.updateArticle(this.article).subscribe( response => { + if (response.errors !== undefined){ + alert('Error when updating article'); + } + else{ + alert('Article successfully updated'); + this.router.navigate(['home/myArticles/1']); + } + }); + } + + + private autocompleteTags(){ + this.http.getAllTags().subscribe( response => this.tags = response.tags); + + this.filteredTags = this.articleForm.get('tagList').valueChanges.pipe( + startWith(''), + map(value => this.filter(value)) + ); + } + + private filter(value: string): string[] { + const filterValue = value.toLowerCase(); + + return this.tags.filter(tag => + tag.toLowerCase().indexOf(filterValue) === 0 && this.selectedTags.indexOf(tag) === -1 + ); + } + + selectTag(tag: string){ + this.selectedTags.push(tag); + this.articleForm.patchValue({tagList: ''}); + } + + deselectTag(tag: string){ + const index = this.selectedTags.indexOf(tag); + this.selectedTags.splice(index, 1); + } + +} diff --git a/src/app/article-page/article-page.component.html b/src/app/article-page/article-page.component.html new file mode 100644 index 0000000..049cc40 --- /dev/null +++ b/src/app/article-page/article-page.component.html @@ -0,0 +1,37 @@ +
+
+
+
+ +
+
+ + + +
+
+ +
+

{{article.body}}

+
+ +
+ + +
+
+ +
+ + + + +
+
\ No newline at end of file diff --git a/src/app/article-page/article-page.component.scss b/src/app/article-page/article-page.component.scss new file mode 100644 index 0000000..f36ce41 --- /dev/null +++ b/src/app/article-page/article-page.component.scss @@ -0,0 +1,104 @@ +.center-page{ + width: 80%; + margin: 0 auto; +} + +.article-group{ + background-color: white; + border: solid 1px lightgray; + margin-top: 20px; + margin-bottom: 20px; + padding: 0px; + overflow: hidden; +} + +.article-header{ + background-color: rgb(54, 54, 54); + padding: 10px; + overflow: hidden; +} + +.author-image{ + float: left; + margin-right: 15px; +} + +.author-image img{ + max-width: 150px; + max-height: 150px; +} + +.header-text{ + margin-left: 15px; +} + +.article-title{ + font-size: xx-large; + font-family: Arial, Helvetica, sans-serif; + font-weight: bold; + color: white; + display: block; + margin: 0px; + margin-bottom: 5px; +} + +.article-subtitle{ + color: lightgray; + font-size: small; + display: block; + margin: 0px; +} + +.article-description{ + color: white; + font-family: cursive; + font-size: large; + margin-top: 20px; + margin-left: 10px; +} + +.article-content{ + padding: 15px; + clear: both; +} + +.article-body{ + word-wrap: break-word; + color: black; + font-family: Arial, Helvetica, sans-serif; +} + +.update-delete-section{ + float: right; +} + +.btn-update{ + padding: 5px 15px; + background-color: rgb(121, 65, 253); + margin: 5px; + color:black; +} + +.btn-update:hover{ + background-color: rgb(151, 107, 255); +} + +.btn-delete{ + padding: 5px 10px; + background-color: rgb(199, 75, 133); + margin: 5px; + color:black; +} + +.btn-delete:hover{ + background-color: rgb(189, 105, 144); +} + +.comments-section{ + width: 80%; + float: right; + background-color: rgb(240, 240, 240); + border-radius: 5px; + padding: 10px; + box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.4); +} \ No newline at end of file diff --git a/src/app/article-page/article-page.component.spec.ts b/src/app/article-page/article-page.component.spec.ts new file mode 100644 index 0000000..f22db81 --- /dev/null +++ b/src/app/article-page/article-page.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ArticlePageComponent } from './article-page.component'; + +describe('ArticlePageComponent', () => { + let component: ArticlePageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ArticlePageComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ArticlePageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/article-page/article-page.component.ts b/src/app/article-page/article-page.component.ts new file mode 100644 index 0000000..0b44223 --- /dev/null +++ b/src/app/article-page/article-page.component.ts @@ -0,0 +1,70 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ArticleService } from '../services/article.service'; +import { Article } from '../model/article'; +import { HttpService } from '../services/http.service'; +import { Comment } from '../model/comment'; +import { User } from '../model/user'; +import { Router } from '@angular/router'; +import { Author } from '../model/author'; +import { LocalStorageService } from '../services/local-storage.service'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-article-page', + templateUrl: './article-page.component.html', + styleUrls: ['./article-page.component.scss'] +}) +export class ArticlePageComponent implements OnInit, OnDestroy { + + article: Article; + comments: Comment[]; + currentUser: User; + + commentSubscription: Subscription; + userSubscription: Subscription; + + constructor( + private articleService: ArticleService, + private http: HttpService, + private router: Router, + private storage: LocalStorageService + ) { } + + ngOnInit(): void { + this.article = this.articleService.getArticle(); + this.updateComments(); + } + + ngOnDestroy(){ + this.commentSubscription.unsubscribe(); + if (this.userSubscription !== undefined){ + this.userSubscription.unsubscribe(); + } + } + + updateComments(){ + this.commentSubscription = this.http.getAllCommentsByArticle(this.article).subscribe( + response => this.comments = response.comments + ); + if (this.storage.getAuthentication()){ + this.userSubscription = this.http.getCurrentUser().subscribe(response => this.currentUser = response.user); + } + } + + updateArticle(){ + this.router.navigate(['editor/update']); + } + + deleteArticle(){ + this.http.deleteArticle(this.article); + } + + belongsToAuthor(author: Author){ + if (this.currentUser !== undefined && author.username === this.currentUser.username){ + return true; + } + else{ + return false; + } + } +} diff --git a/src/app/articles-list/articles-list.component.html b/src/app/articles-list/articles-list.component.html new file mode 100644 index 0000000..6aef8fa --- /dev/null +++ b/src/app/articles-list/articles-list.component.html @@ -0,0 +1,33 @@ +
    +
  • +
    + +
    +
    +
    + {{article.title}} +
    +
    + {{article.createdAt | date:'medium'}} +
    + +
    +

    {{article.description}}

    +
    + + More +
    +
    + +
    +
    + +
    +
    +
    +
  • +
  • There are no articles yet
  • +
\ No newline at end of file diff --git a/src/app/articles-list/articles-list.component.scss b/src/app/articles-list/articles-list.component.scss new file mode 100644 index 0000000..cf2a448 --- /dev/null +++ b/src/app/articles-list/articles-list.component.scss @@ -0,0 +1,93 @@ +.articles-container{ + margin: 0px; + padding: 0px; +} + +.article-group{ + list-style: none; + background-color: white; + min-height: 100px; + border-top: solid 1px rgba($color: #000000, $alpha: 0.4); + margin-top: 10px; + padding: 40px; + clear: both; + overflow: hidden; +} + +.author-image{ + width: 50px; + height: 50px; + margin-right: 10px; + border-radius: 50%; +} + +.align-left{ + float: left; +} + +.align-right{ + float: right; +} + +.clear{ + clear: both; +} + +.article-description{ + margin-top: 10px; + border-top: solid 1px rgb(204, 202, 202); + word-wrap: break-word; +} + +.article-title{ + font-size: large; + font-weight: bold; + font-family: Arial, Helvetica, sans-serif; + +} + +.article-creation-date{ + font-size: small; + color: rgb(187, 187, 187); +} + +.link-more{ + float: right; + color: gray; +} + +.update-delete-section{ + float: right; + color: black; + margin-right: 20px; +} + +.update{ + margin-right: 3px; + display: inline-block; +} + +.update:hover{ + cursor: pointer; + box-shadow: 2px 2px 10px rgba(24, 121, 0, 0.8); +} + +.delete{ + margin-left: 3px; + display: inline-block; +} + +.delete:hover{ + cursor: pointer; + box-shadow: 2px 2px 10px rgba(189, 0, 0, 0.8); +} + +.no-articles{ + background-color: white; + padding: 20px; + margin-top: 40px; + color: rgb(41, 41, 41); + font-size: large; + list-style: none; + text-align: center; +} \ No newline at end of file diff --git a/src/app/articles-list/articles-list.component.spec.ts b/src/app/articles-list/articles-list.component.spec.ts new file mode 100644 index 0000000..91aa8f2 --- /dev/null +++ b/src/app/articles-list/articles-list.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ArticlesListComponent } from './articles-list.component'; + +describe('ArticlesListComponent', () => { + let component: ArticlesListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ArticlesListComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ArticlesListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/articles-list/articles-list.component.ts b/src/app/articles-list/articles-list.component.ts new file mode 100644 index 0000000..49eb363 --- /dev/null +++ b/src/app/articles-list/articles-list.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit, Input, OnDestroy } from '@angular/core'; +import { MultipleArticles, Article } from '../model/article'; +import { Router } from '@angular/router'; +import { ArticleService } from '../services/article.service'; +import { User } from '../model/user'; +import { faEdit, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { HttpService } from '../services/http.service'; +import { Author } from '../model/author'; +import { LocalStorageService } from '../services/local-storage.service'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-articles-list', + templateUrl: './articles-list.component.html', + styleUrls: ['./articles-list.component.scss'] +}) +export class ArticlesListComponent implements OnInit, OnDestroy { + + updateIcon = faEdit; + deleteIcon = faTrash; + currentUser: User; + @Input() articles: Article[]; + + userSubscription: Subscription; + + constructor( + private router: Router, + private articleService: ArticleService, + private http: HttpService, + private storage: LocalStorageService + ) { } + + ngOnInit(): void { + if (this.storage.getAuthentication()){ + this.userSubscription = this.http.getCurrentUser().subscribe(response => this.currentUser = response.user); + } + } + + ngOnDestroy(){ + if (this.userSubscription !== undefined){ + this.userSubscription.unsubscribe(); + } + } + + goToArticlePage(article: Article){ + this.articleService.setArticle(article); + this.router.navigate(['article']); + } + + updateArticle(article: Article){ + this.articleService.setArticle(article); + this.router.navigate(['editor/update']); + } + + deleteArticle(article: Article){ + this.http.deleteArticle(article); + this.router.navigate(['home/myArticles/1']); + } + + belongsToAuthor(author: Author){ + if (this.currentUser !== undefined && author.username === this.currentUser.username){ + return true; + } + else{ + return false; + } + } + +} diff --git a/src/app/comment-form/comment-form.component.html b/src/app/comment-form/comment-form.component.html new file mode 100644 index 0000000..3f22db7 --- /dev/null +++ b/src/app/comment-form/comment-form.component.html @@ -0,0 +1,11 @@ +
+
+
+ +
+ + +
+
\ No newline at end of file diff --git a/src/app/comment-form/comment-form.component.scss b/src/app/comment-form/comment-form.component.scss new file mode 100644 index 0000000..adfcade --- /dev/null +++ b/src/app/comment-form/comment-form.component.scss @@ -0,0 +1,3 @@ +.btn-margin{ + margin-left: 10px; +} \ No newline at end of file diff --git a/src/app/comment-form/comment-form.component.spec.ts b/src/app/comment-form/comment-form.component.spec.ts new file mode 100644 index 0000000..57c0f75 --- /dev/null +++ b/src/app/comment-form/comment-form.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CommentFormComponent } from './comment-form.component'; + +describe('CommentFormComponent', () => { + let component: CommentFormComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CommentFormComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommentFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/comment-form/comment-form.component.ts b/src/app/comment-form/comment-form.component.ts new file mode 100644 index 0000000..8c64190 --- /dev/null +++ b/src/app/comment-form/comment-form.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, Output, EventEmitter, OnDestroy } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { HttpService } from '../services/http.service'; +import { LocalStorageService } from '../services/local-storage.service'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-comment-form', + templateUrl: './comment-form.component.html', + styleUrls: ['./comment-form.component.scss'] +}) +export class CommentFormComponent implements OnDestroy { + + commentForm = new FormGroup({ + body: new FormControl('', [Validators.required, Validators.maxLength(500)]) + }); + + @Input() article; + @Output() commentSent = new EventEmitter(); + + commentSubscription: Subscription; + + constructor( + private http: HttpService, + private storage: LocalStorageService, + private router: Router + ) { } + + ngOnDestroy(){ + this.commentSubscription.unsubscribe(); + } + + sendComment(){ + if (!this.storage.getAuthentication()){ + alert('You need to log in'); + this.router.navigate(['login']); + } + else{ + this.commentSubscription = this.http.addCommentToArticle(this.article, this.commentForm.value).subscribe( response => { + if (response.errors !== undefined){ + alert('Error when commenting'); + } + else{ + this.commentSent.emit(this.article); + } + }); + this.commentForm.reset(); + } + + } +} diff --git a/src/app/comments-list/comments-list.component.html b/src/app/comments-list/comments-list.component.html new file mode 100644 index 0000000..9d95d2b --- /dev/null +++ b/src/app/comments-list/comments-list.component.html @@ -0,0 +1,27 @@ +
    +
  • +
    + +
    +
    +
    + {{comment.author.username}} + {{comment.createdAt | date:'medium'}} +
    + +
    +

    {{comment.body}}

    +
    + +
    +
    + +
    +
    +
    +
  • +
  • Be the first to comment
  • +
diff --git a/src/app/comments-list/comments-list.component.scss b/src/app/comments-list/comments-list.component.scss new file mode 100644 index 0000000..24e70ec --- /dev/null +++ b/src/app/comments-list/comments-list.component.scss @@ -0,0 +1,85 @@ + +.comments-container{ + margin: 5px; + margin-top: 20px; + padding: 0px; + min-height: 50px; + display: block; +} + +.comment-group{ + list-style: none; + background-color: white; + border-radius: 2px; + border-top: solid 1px rgba($color: #000000, $alpha: 0.4); + margin-top: 10px; + padding: 10px; + clear: both; + overflow: hidden; +} + +.author-image{ + width: 20px; + margin-right: 10px; + border-radius: 50%; +} + +.image-container{ + height: 100%; + float: left; +} + +.align-left{ + float: left; +} + +.align-right{ + float: right; +} + +.clear{ + clear: both; +} + +.comment-body{ + margin-top: 10px; + word-wrap: break-word; +} + +.comment-author{ + font-size: medium; + font-weight: bold; + font-family: Arial, Helvetica, sans-serif; +} + +.comment-creation-date{ + font-size: small; + color: rgb(187, 187, 187); + margin-left: 10px; +} + +.update-delete-section{ + float: right; + color: black; + margin-right: 20px; +} + +.delete{ + margin-left: 3px; + display: inline-block; +} + +.delete:hover{ + cursor: pointer; + box-shadow: 2px 2px 10px rgba(189, 0, 0, 0.8); +} + +.no-comments{ + background-color: white; + padding: 20px; + margin-top: 40px; + color: rgb(41, 41, 41); + font-size: large; + list-style: none; + text-align: center; +} \ No newline at end of file diff --git a/src/app/comments-list/comments-list.component.spec.ts b/src/app/comments-list/comments-list.component.spec.ts new file mode 100644 index 0000000..7d816e5 --- /dev/null +++ b/src/app/comments-list/comments-list.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CommentsListComponent } from './comments-list.component'; + +describe('CommentsListComponent', () => { + let component: CommentsListComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CommentsListComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommentsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/comments-list/comments-list.component.ts b/src/app/comments-list/comments-list.component.ts new file mode 100644 index 0000000..4139f43 --- /dev/null +++ b/src/app/comments-list/comments-list.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angular/core'; +import { HttpService } from '../services/http.service'; +import { Comment } from '../model/comment'; +import { User } from '../model/user'; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { Author } from '../model/author'; +import { LocalStorageService } from '../services/local-storage.service'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-comments-list', + templateUrl: './comments-list.component.html', + styleUrls: ['./comments-list.component.scss'] +}) +export class CommentsListComponent implements OnInit, OnDestroy { + + currentUser: User; + deleteIcon = faTrash; + userSubscription: Subscription; + @Input() comments; + @Input() article; + @Output() commentDeleted = new EventEmitter(); + + constructor( + private http: HttpService, + private storage: LocalStorageService + ) { } + + ngOnInit(): void { + if (this.storage.getAuthentication()){ + this.http.getCurrentUser().subscribe( response => this.currentUser = response.user); + } + } + + ngOnDestroy(){ + if (this.userSubscription !== undefined){ + this.userSubscription.unsubscribe(); + } + } + + deleteComment(comment: Comment){ + this.http.deleteComment(this.article, comment).subscribe( () => this.commentDeleted.emit() ); + } + + belongsToAuthor(author: Author){ + if (this.currentUser !== undefined && author.username === this.currentUser.username){ + return true; + } + else{ + return false; + } + } +} diff --git a/src/app/error-page/error-page.component.html b/src/app/error-page/error-page.component.html new file mode 100644 index 0000000..740d642 --- /dev/null +++ b/src/app/error-page/error-page.component.html @@ -0,0 +1,9 @@ +
+
+
+

404

+

Page not found

+
+ Back to home page +
+
diff --git a/src/app/error-page/error-page.component.scss b/src/app/error-page/error-page.component.scss new file mode 100644 index 0000000..7ed0bcf --- /dev/null +++ b/src/app/error-page/error-page.component.scss @@ -0,0 +1,104 @@ +* { + -webkit-box-sizing: border-box; + box-sizing: border-box; + } + + body { + padding: 0; + margin: 0; + } + + #notfound { + position: relative; + height: 100vh; + background: #030005; + } + + #notfound .notfound { + position: absolute; + left: 50%; + top: 50%; + -webkit-transform: translate(-50%, -50%); + -ms-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + } + + .notfound { + max-width: 767px; + width: 100%; + line-height: 1.4; + text-align: center; + } + + .notfound .notfound-404 { + position: relative; + height: 180px; + margin-bottom: 20px; + z-index: -1; + } + + .notfound .notfound-404 h1 { + font-family: 'Montserrat', sans-serif; + position: absolute; + left: 50%; + top: 50%; + -webkit-transform: translate(-50% , -50%); + -ms-transform: translate(-50% , -50%); + transform: translate(-50% , -50%); + font-size: 224px; + font-weight: 900; + margin-top: 0px; + margin-bottom: 0px; + margin-left: -12px; + color: #030005; + text-transform: uppercase; + text-shadow: -1px -1px 0px #8400ff, 1px 1px 0px #ff005a; + letter-spacing: -20px; + } + + + .notfound .notfound-404 h2 { + font-family: 'Montserrat', sans-serif; + position: absolute; + left: 0; + right: 0; + top: 110px; + font-size: 42px; + font-weight: 700; + color: #fff; + text-transform: uppercase; + text-shadow: 0px 2px 0px #8400ff; + letter-spacing: 13px; + margin: 0; + } + + .notfound a { + font-family: 'Montserrat', sans-serif; + display: inline-block; + text-transform: uppercase; + color: #ff005a; + text-decoration: none; + border: 2px solid; + background: transparent; + padding: 10px 40px; + font-size: 14px; + font-weight: 700; + -webkit-transition: 0.2s all; + transition: 0.2s all; + } + + .notfound a:hover { + color: #8400ff; + } + + @media only screen and (max-width: 767px) { + .notfound .notfound-404 h2 { + font-size: 24px; + } + } + + @media only screen and (max-width: 480px) { + .notfound .notfound-404 h1 { + font-size: 182px; + } + } \ No newline at end of file diff --git a/src/app/error-page/error-page.component.spec.ts b/src/app/error-page/error-page.component.spec.ts new file mode 100644 index 0000000..faaa44d --- /dev/null +++ b/src/app/error-page/error-page.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ErrorPageComponent } from './error-page.component'; + +describe('ErrorPageComponent', () => { + let component: ErrorPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ErrorPageComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ErrorPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/error-page/error-page.component.ts b/src/app/error-page/error-page.component.ts new file mode 100644 index 0000000..58fe68f --- /dev/null +++ b/src/app/error-page/error-page.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-error-page', + templateUrl: './error-page.component.html', + styleUrls: ['./error-page.component.scss'] +}) +export class ErrorPageComponent { + +} diff --git a/src/app/home-page/home-page.component.html b/src/app/home-page/home-page.component.html new file mode 100644 index 0000000..8e2b0f1 --- /dev/null +++ b/src/app/home-page/home-page.component.html @@ -0,0 +1,37 @@ +
+
+ + + + + + + +
+ + + +
    + +
+
\ No newline at end of file diff --git a/src/app/home-page/home-page.component.scss b/src/app/home-page/home-page.component.scss new file mode 100644 index 0000000..6890a8b --- /dev/null +++ b/src/app/home-page/home-page.component.scss @@ -0,0 +1,70 @@ +.center-page{ + width: 80%; + margin: 0 auto; + margin-top: 20px; +} + +.articles-section{ + width: 75%; + background-color: rgb(243, 243, 243); + border-radius: 5px; + float: left; + padding: 20px; +} + +.tags-section{ + width: 20%; + background-color: rgb(223, 223, 223); + border-radius: 30px; + box-shadow: 2px 2px 5px 4px rgba(0, 0, 0, 0.4); + float: right; + padding: 10px; +} + +.tags-container{ + margin: 0px; + min-height: 180px; +} + +.tag{ + background-color: rgb(141, 141, 141); + padding: 5px; + float: left; + list-style-type: none; + margin: 2px auto; + margin-left: 2px; + border-radius: 15px; +} + +.btn-article{ + background-color: rgb(216, 88, 255); + box-shadow: 2px 2px 5px rgba(210, 63, 255, 0.8); + margin: 5px; +} + +.btn-article:hover{ + background-color: rgb(205, 48, 253); +} + +.btn-article-active{ + border-bottom: solid 3px rgb(144, 20, 245); +} + +.paginator{ + display: block; + width: 75%; + background-color: rgb(231, 231, 231); + border-radius: 5px; + clear: both; + list-style-type: none; + padding: 10px; +} + +.page{ + background-color: rgb(255, 104, 247); + padding: 5px; + border-radius: 50%; + display: inline-block; + margin: 2px; + width: 40px; +} \ No newline at end of file diff --git a/src/app/home-page/home-page.component.spec.ts b/src/app/home-page/home-page.component.spec.ts new file mode 100644 index 0000000..e180332 --- /dev/null +++ b/src/app/home-page/home-page.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomePageComponent } from './home-page.component'; + +describe('HomePageComponent', () => { + let component: HomePageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HomePageComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomePageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/home-page/home-page.component.ts b/src/app/home-page/home-page.component.ts new file mode 100644 index 0000000..fddad1a --- /dev/null +++ b/src/app/home-page/home-page.component.ts @@ -0,0 +1,93 @@ +import { Component, OnInit, OnDestroy, OnChanges } from '@angular/core'; +import { HttpService } from '../services/http.service'; +import { Article } from '../model/article'; +import { Router, ActivatedRoute } from '@angular/router'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-home-page', + templateUrl: './home-page.component.html', + styleUrls: ['./home-page.component.scss'] +}) +export class HomePageComponent implements OnInit, OnDestroy { + + articles: Article[] = []; + tags: string[] = []; + readonly pageLimit = 10; + pages: number[] = []; + articlesMode = 'all'; + page = 1; + paramsSubscription: Subscription; + + callbackPaginator = response => { + this.articles = response.articles; + const pagesCount = (Math.ceil(response.articlesCount / this.pageLimit)); + + this.pages = []; + for (let i = 0; i < pagesCount; i++){ + this.pages.push(i + 1); + } + } + + constructor( + private http: HttpService, + private router: Router, + private route: ActivatedRoute + ) { } + + ngOnInit(): void { + this.paramsSubscription = this.route.params.subscribe(params => { + this.articlesMode = params.articlesMode; + this.page = params.page; + }); + + this.getAllTags(); + + switch (this.articlesMode){ + case undefined: + this.router.navigate(['home/all/1']); + this.http.getAllArticles(this.page, this.pageLimit).subscribe(this.callbackPaginator); + break; + case 'all': + this.http.getAllArticles(this.page, this.pageLimit).subscribe(this.callbackPaginator); + break; + case 'myArticles': + this.http.getMyArticles(this.page, this.pageLimit, this.callbackPaginator); + break; + default: + this.router.navigate(['error']); + } + } + + ngOnDestroy(){ + this.paramsSubscription.unsubscribe(); + } + + getAllArticles(page: number){ + this.router.navigate(['home/all/' + page]); + this.http.getAllArticles(this.page, this.pageLimit).subscribe(this.callbackPaginator); + } + + getAllTags(){ + this.tags = []; + this.http.getAllTags().subscribe(response => this.tags = response.tags); + } + + getMyArticles(page: number){ + this.router.navigate(['home/myArticles/' + page]); + this.http.getMyArticles(this.page, this.pageLimit, this.callbackPaginator); + } + + changePage(page: number){ + switch (this.articlesMode){ + case 'all': + this.http.getAllArticles(page, this.pageLimit).subscribe(this.callbackPaginator); + this.router.navigate(['home/all/' + page]); + break; + case 'myArticles': + this.http.getMyArticles(page, this.pageLimit, this.callbackPaginator); + this.router.navigate(['home/myArticles/' + page]); + break; + } + } +} diff --git a/src/app/model/article.ts b/src/app/model/article.ts new file mode 100644 index 0000000..819a28c --- /dev/null +++ b/src/app/model/article.ts @@ -0,0 +1,24 @@ +import { Author } from './author'; +import { BaseInterface } from './base-interface'; + +export interface SingleArticle extends BaseInterface { + article: Article; +} + +export interface MultipleArticles extends BaseInterface { + articles: Article[]; + articlesCount: number; +} + +export interface Article { + slug: string; + title: string; + description: string; + body: string; + tagList: string[]; + createdAt: Date; + updatedAt: Date; + favorited: boolean; + favoritesCount: number; + author: Author; +} diff --git a/src/app/model/author.ts b/src/app/model/author.ts new file mode 100644 index 0000000..d546c61 --- /dev/null +++ b/src/app/model/author.ts @@ -0,0 +1,6 @@ +export interface Author { + username: string; + bio: string; + image: string; + following: boolean; +} diff --git a/src/app/model/base-interface.ts b/src/app/model/base-interface.ts new file mode 100644 index 0000000..21aa51e --- /dev/null +++ b/src/app/model/base-interface.ts @@ -0,0 +1,10 @@ +export interface BaseInterface { + errors: Errors; +} + +interface Errors{ + body: string[]; + username: string[]; + password: string[]; + email: string[]; +} diff --git a/src/app/model/comment.ts b/src/app/model/comment.ts new file mode 100644 index 0000000..1d7257f --- /dev/null +++ b/src/app/model/comment.ts @@ -0,0 +1,14 @@ +import { Author } from './author'; +import { BaseInterface } from './base-interface'; + +export interface MultipleComments extends BaseInterface{ + comments: Comment[]; +} + +export interface Comment { + id: number; + createdAt: Date; + updatedAt: Date; + body: string; + author: Author; +} diff --git a/src/app/model/user.ts b/src/app/model/user.ts new file mode 100644 index 0000000..39760c4 --- /dev/null +++ b/src/app/model/user.ts @@ -0,0 +1,13 @@ +import { BaseInterface } from './base-interface'; + +export interface SingleUser extends BaseInterface{ + user: User; +} + +export interface User { + email: string; + token: string; + username: string; + bio: string; + image: string; +} diff --git a/src/app/services/article.service.spec.ts b/src/app/services/article.service.spec.ts new file mode 100644 index 0000000..120c0e2 --- /dev/null +++ b/src/app/services/article.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ArticleService } from './article.service'; + +describe('ArticleService', () => { + let service: ArticleService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ArticleService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/article.service.ts b/src/app/services/article.service.ts new file mode 100644 index 0000000..19e3f5f --- /dev/null +++ b/src/app/services/article.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { Article } from '../model/article'; +import { LocalStorageService } from './local-storage.service'; + +@Injectable({ + providedIn: 'root' +}) +export class ArticleService { + + readonly articleKey = 'article'; + article: Article; + + constructor( + private storage: LocalStorageService + ) { } + + setArticle(article: Article){ + this.article = article; + this.storage.set(this.articleKey, article); + } + + getArticle(){ + if (!this.article){ + this.article = this.storage.get(this.articleKey); + } + return this.article; + } +} diff --git a/src/app/services/http.service.spec.ts b/src/app/services/http.service.spec.ts new file mode 100644 index 0000000..7b345d4 --- /dev/null +++ b/src/app/services/http.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { HttpService } from './http.service'; + +describe('HttpService', () => { + let service: HttpService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(HttpService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/http.service.ts b/src/app/services/http.service.ts new file mode 100644 index 0000000..eae3380 --- /dev/null +++ b/src/app/services/http.service.ts @@ -0,0 +1,144 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { MultipleArticles, Article, SingleArticle } from '../model/article'; +import { SingleUser, User } from '../model/user'; +import { Author } from '../model/author'; +import { MultipleComments, Comment } from '../model/comment'; +import { LocalStorageService } from './local-storage.service'; +import { Observable, Subscription } from 'rxjs'; +import { BaseInterface } from '../model/base-interface'; + + +@Injectable({ + providedIn: 'root' +}) +export class HttpService implements OnDestroy { + + readonly baseUrl = 'https://conduit.productionready.io/api/'; + + getMyArticlesSubscription: Subscription; + deleteArticleSubscription: Subscription; + deleteCommentSubscription: Subscription; + registerUserSubscription: Subscription; + loginSubscription: Subscription; + + constructor( + private http: HttpClient, + private storage: LocalStorageService + ) { } + + ngOnDestroy(){ + this.getMyArticlesSubscription.unsubscribe(); + this.deleteArticleSubscription.unsubscribe(); + this.deleteCommentSubscription.unsubscribe(); + this.registerUserSubscription.unsubscribe(); + this.loginSubscription.unsubscribe(); + } + + getAllArticles(page: number, limit: number){ + const offset = (page - 1) * limit; + const url = `${this.baseUrl}articles?limit=${limit}&offset=${offset}`; + return this.http.get(url); + } + + getMyArticles(page: number, limit: number, callback){ + const offset = (page - 1) * limit; + + this.getMyArticlesSubscription = this.getCurrentUser().subscribe( ({user}) => { + const url = `${this.baseUrl}articles?author=${user.username}&limit=${limit}&offset=${offset}`; + this.http.get(url).subscribe(callback); + }); + } + + getArticlesByTag(tag: string) { + const limit = 20; + const url = `${this.baseUrl}articles?tag=${tag}&limit=${limit}`; + return this.http.get(url); + } + + createArticle(article: Article){ + const url = `${this.baseUrl}articles`; + const token = this.storage.getAuthentication(); + const headers = {Authorization: 'Token ' + token}; + return this.http.post(url, {article}, {headers}); + } + + updateArticle(article: Article){ + const url = `${this.baseUrl}articles/${article.slug}`; + const token = this.storage.getAuthentication(); + const headers = {Authorization: 'Token ' + token}; + return this.http.put(url, {article}, {headers}); + } + + deleteArticle(article: Article){ + const url = `${this.baseUrl}articles/${article.slug}`; + const token = this.storage.getAuthentication(); + const headers = {Authorization: 'Token ' + token}; + this.deleteArticleSubscription = this.http.delete(url, {headers}).subscribe( + response => { + if (response.errors !== undefined){ + alert('Error when deleting article'); + } + }); + } + + getAllTags() { + const url = `${this.baseUrl}tags`; + return this.http.get(url); + } + + addCommentToArticle(article: Article, comment: Comment){ + const url = `${this.baseUrl}articles/${article.slug}/comments`; + const token = this.storage.getAuthentication(); + const headers = {Authorization: 'Token ' + token}; + return this.http.post(url, {comment}, {headers}); + } + + deleteComment(article: Article, comment: Comment){ + const url = `${this.baseUrl}articles/${article.slug}/comments/${comment.id}`; + const token = this.storage.getAuthentication(); + const headers = {Authorization: 'Token ' + token}; + const observable = this.http.delete(url, {headers}); + this.deleteCommentSubscription = observable.subscribe( response => { + if (response.errors !== undefined){ + alert('Error when deleting comment'); + } + }); + + return observable; + } + + getAllCommentsByArticle(article: Article){ + const url = `${this.baseUrl}articles/${article.slug}/comments`; + return this.http.get(url); + } + + getCurrentUser(){ + const url = this.baseUrl + 'user'; + const token = this.storage.getAuthentication(); + const headers = {Authorization: 'Token ' + token}; + return this.http.get(url, {headers}); + } + + registerUser(user: User){ + const url = `${this.baseUrl}users`; + const observable = this.http.post(url, {user}); + this.registerUserSubscription = observable.subscribe(response => { + if (response.errors === undefined){ + this.storage.saveLogIn(response.user); + } + }); + return observable; + } + + logIn(user: User){ + const url = `${this.baseUrl}users/login`; + const observable = this.http.post(url, {user}); + this.loginSubscription = observable.subscribe(response => this.storage.saveLogIn(response.user)); + return observable; + } + + logOut(){ + this.storage.logOut(); + } +} diff --git a/src/app/services/local-storage.service.spec.ts b/src/app/services/local-storage.service.spec.ts new file mode 100644 index 0000000..ba1dbd4 --- /dev/null +++ b/src/app/services/local-storage.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { LocalStorageService } from './local-storage.service'; + +describe('LocalStorageService', () => { + let service: LocalStorageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LocalStorageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/local-storage.service.ts b/src/app/services/local-storage.service.ts new file mode 100644 index 0000000..ac49422 --- /dev/null +++ b/src/app/services/local-storage.service.ts @@ -0,0 +1,34 @@ +import { Injectable, Inject } from '@angular/core'; +import { User } from '../model/user'; +import {LOCAL_STORAGE, WebStorageService} from 'ngx-webstorage-service'; + +@Injectable({ + providedIn: 'root' +}) +export class LocalStorageService { + + readonly appID = 'RWA'; + readonly currentUserID = 'current-user'; + + constructor(@Inject(LOCAL_STORAGE) private storage: WebStorageService) { } + + saveLogIn(user: User){ + this.storage.set(this.appID + this.currentUserID, user.token); + } + + getAuthentication(){ + return this.storage.get(this.appID + this.currentUserID); + } + + logOut(){ + this.storage.remove(this.appID + this.currentUserID); + } + + set(key, value){ + this.storage.set(this.appID + key, value); + } + + get(key){ + return this.storage.get(this.appID + key); + } +} diff --git a/src/app/signin/signin.component.html b/src/app/signin/signin.component.html new file mode 100644 index 0000000..d0ac39e --- /dev/null +++ b/src/app/signin/signin.component.html @@ -0,0 +1,18 @@ +
+

Log In

+
+
+ + +
+ +
+ + +
+ + +
+
diff --git a/src/app/signin/signin.component.scss b/src/app/signin/signin.component.scss new file mode 100644 index 0000000..e63ba25 --- /dev/null +++ b/src/app/signin/signin.component.scss @@ -0,0 +1,17 @@ +.center-form{ + width: 30%; + margin: 0 auto; + margin-top: 100px; + margin-bottom: 20px; + padding: 30px; + border-radius: 5px; + box-shadow: 0px 0px 15px rgba($color: #000000, $alpha: 0.6); + background-color: white; +} + +.title{ + font-size: xx-large; + font-weight: bold; + font-family: Arial, Helvetica, sans-serif; + text-align: center; +} \ No newline at end of file diff --git a/src/app/signin/signin.component.spec.ts b/src/app/signin/signin.component.spec.ts new file mode 100644 index 0000000..c64b0b2 --- /dev/null +++ b/src/app/signin/signin.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SigninComponent } from './signin.component'; + +describe('SigninComponent', () => { + let component: SigninComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SigninComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SigninComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/signin/signin.component.ts b/src/app/signin/signin.component.ts new file mode 100644 index 0000000..a703ab7 --- /dev/null +++ b/src/app/signin/signin.component.ts @@ -0,0 +1,41 @@ +import { Component, Output, EventEmitter, OnDestroy } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { HttpService } from '../services/http.service'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-signin', + templateUrl: './signin.component.html', + styleUrls: ['./signin.component.scss'] +}) +export class SigninComponent implements OnDestroy { + + @Output() loggingIn = new EventEmitter(); + + signinForm = new FormGroup({ + password: new FormControl('', [Validators.required]), + email: new FormControl('', [Validators.required, Validators.email]) + }); + + loginSubscription: Subscription; + + constructor( + private http: HttpService, + private router: Router + ) { } + + ngOnDestroy(){ + if (this.loginSubscription !== undefined){ + this.loginSubscription.unsubscribe(); + } + } + + logIn(){ + this.loginSubscription = this.http.logIn(this.signinForm.value).subscribe( + response => this.loggingIn.emit(response.user.token) + ); + this.router.navigate(['home/all/1']); + } + +} diff --git a/src/app/signup/signup.component.html b/src/app/signup/signup.component.html new file mode 100644 index 0000000..ae47a99 --- /dev/null +++ b/src/app/signup/signup.component.html @@ -0,0 +1,32 @@ +
+

Sing Up

+
+
+ + +
+ *{{error}} +
+
+ +
+ + +
+ *{{error}} +
+
+ +
+ + +
+ *{{error}} +
+
+ + +
+
\ No newline at end of file diff --git a/src/app/signup/signup.component.scss b/src/app/signup/signup.component.scss new file mode 100644 index 0000000..dead57a --- /dev/null +++ b/src/app/signup/signup.component.scss @@ -0,0 +1,21 @@ +.center-form{ + width: 30%; + margin: 0 auto; + margin-top: 100px; + margin-bottom: 20px; + padding: 30px; + border-radius: 5px; + box-shadow: 0px 0px 15px rgba($color: #000000, $alpha: 0.6); + background-color: white; +} + +.error-message{ + color: tomato; +} + +.title{ + font-size: xx-large; + font-weight: bold; + font-family: Arial, Helvetica, sans-serif; + text-align: center; +} \ No newline at end of file diff --git a/src/app/signup/signup.component.spec.ts b/src/app/signup/signup.component.spec.ts new file mode 100644 index 0000000..43e46a5 --- /dev/null +++ b/src/app/signup/signup.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SignupComponent } from './signup.component'; + +describe('SignupComponent', () => { + let component: SignupComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ SignupComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SignupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/signup/signup.component.ts b/src/app/signup/signup.component.ts new file mode 100644 index 0000000..03fb5cc --- /dev/null +++ b/src/app/signup/signup.component.ts @@ -0,0 +1,38 @@ +import { Component, OnDestroy } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { HttpService } from '../services/http.service'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-signup', + templateUrl: './signup.component.html', + styleUrls: ['./signup.component.scss'] +}) +export class SignupComponent { + + signupForm = new FormGroup({ + username: new FormControl('', [Validators.required]), + password: new FormControl('', [Validators.required]), + email: new FormControl('', [Validators.required, Validators.email]) + }); + + formErrors: any = {username: undefined, password: undefined, email: undefined}; + + constructor( + private http: HttpService, + private router: Router + ) { } + + register(){ + this.http.registerUser(this.signupForm.value).subscribe(response => { + if (response.errors === undefined){ + this.router.navigate(['home/all/1']); + } + else{ + this.formErrors = response.errors; + } + }); + } + +} diff --git a/src/index.html b/src/index.html index 62a7817..bf371f1 100644 --- a/src/index.html +++ b/src/index.html @@ -2,12 +2,14 @@ - NgPeti + RealWorld App + + - + diff --git a/src/polyfills.ts b/src/polyfills.ts index 03711e5..01e24d6 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -1,3 +1,7 @@ +/*************************************************************************************************** + * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. + */ +import '@angular/localize/init'; /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. diff --git a/src/styles.scss b/src/styles.scss index 90d4ee0..4555dca 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1 +1,13 @@ /* You can add global styles to this file, and also import other style files */ + +/* Importing Bootstrap SCSS file. */ +@import '~bootstrap/scss/bootstrap'; + +html, body{ + height:100%; + margin:0; + display: flex; + flex-direction: column; +} +html, body { height: 100%; } +body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }