From ef93d51f77ba5f20fe84db82a8d59a1b02fda7d5 Mon Sep 17 00:00:00 2001 From: Vasa Date: Sun, 24 Aug 2025 17:24:40 +0300 Subject: [PATCH] large update wip --- README.md | 14 +- angular.json | 3 +- package-lock.json | 1012 ++++++++++++++++- package.json | 13 +- src/app/app.component.ts | 5 +- src/app/app.routes.ts | 16 +- .../application-list.component.html | 5 - .../application-list.component.scss | 9 - .../application-list.component.ts | 23 - .../application/application.component.html | 1 - .../application/application.component.scss | 0 src/app/application/application.component.ts | 28 - src/app/candidate-data.service.ts | 23 - .../application-list.component.html | 99 ++ .../application-list.component.scss | 187 +++ .../application-list.component.ts | 212 ++++ .../application/application.component.html | 150 +++ .../application/application.component.scss | 417 +++++++ .../application/application.component.ts | 170 +++ .../image-input/image-input.component.html | 40 + .../image-input/image-input.component.scss | 75 ++ .../image-input/image-input.component.ts | 78 ++ .../components/landing/landing.component.html | 14 + .../components/landing/landing.component.scss | 115 ++ .../landing/landing.component.ts | 2 +- .../leaflet-map/leaflet-map.component.html | 23 + .../leaflet-map/leaflet-map.component.scss | 9 + .../leaflet-map/leaflet-map.component.ts | 114 ++ .../registration/registration.component.html | 114 ++ .../registration/registration.component.scss | 190 ++++ .../registration/registration.component.ts | 174 +++ .../image-input/image-input.component.html | 28 - .../image-input/image-input.component.scss | 49 - src/app/image-input/image-input.component.ts | 47 - src/app/landing/landing.component.html | 7 - src/app/landing/landing.component.scss | 0 .../page-not-found.component.html | 7 + .../page-not-found.component.scss | 94 ++ .../page-not-found.component.ts | 17 + .../registration/registration.component.html | 115 -- .../registration/registration.component.scss | 164 --- .../registration/registration.component.ts | 85 -- src/app/services/candidate-data.service.ts | 46 + src/app/services/socket-io.service.ts | 35 + src/app/shared/cities.ts | 28 + src/app/validators/city.validator.ts | 11 + src/environments/environment.development.ts | 4 +- src/environments/environment.remote-api.ts | 4 +- src/environments/environment.ts | 3 +- src/index.html | 1 + 50 files changed, 3459 insertions(+), 621 deletions(-) delete mode 100644 src/app/application-list/application-list.component.html delete mode 100644 src/app/application-list/application-list.component.scss delete mode 100644 src/app/application-list/application-list.component.ts delete mode 100644 src/app/application/application.component.html delete mode 100644 src/app/application/application.component.scss delete mode 100644 src/app/application/application.component.ts delete mode 100644 src/app/candidate-data.service.ts create mode 100644 src/app/components/application-list/application-list.component.html create mode 100644 src/app/components/application-list/application-list.component.scss create mode 100644 src/app/components/application-list/application-list.component.ts create mode 100644 src/app/components/application/application.component.html create mode 100644 src/app/components/application/application.component.scss create mode 100644 src/app/components/application/application.component.ts create mode 100644 src/app/components/image-input/image-input.component.html create mode 100644 src/app/components/image-input/image-input.component.scss create mode 100644 src/app/components/image-input/image-input.component.ts create mode 100644 src/app/components/landing/landing.component.html create mode 100644 src/app/components/landing/landing.component.scss rename src/app/{ => components}/landing/landing.component.ts (77%) create mode 100644 src/app/components/leaflet-map/leaflet-map.component.html create mode 100644 src/app/components/leaflet-map/leaflet-map.component.scss create mode 100644 src/app/components/leaflet-map/leaflet-map.component.ts create mode 100644 src/app/components/registration/registration.component.html create mode 100644 src/app/components/registration/registration.component.scss create mode 100644 src/app/components/registration/registration.component.ts delete mode 100644 src/app/image-input/image-input.component.html delete mode 100644 src/app/image-input/image-input.component.scss delete mode 100644 src/app/image-input/image-input.component.ts delete mode 100644 src/app/landing/landing.component.html delete mode 100644 src/app/landing/landing.component.scss create mode 100644 src/app/page-not-found/page-not-found.component.html create mode 100644 src/app/page-not-found/page-not-found.component.scss create mode 100644 src/app/page-not-found/page-not-found.component.ts delete mode 100644 src/app/registration/registration.component.html delete mode 100644 src/app/registration/registration.component.scss delete mode 100644 src/app/registration/registration.component.ts create mode 100644 src/app/services/candidate-data.service.ts create mode 100644 src/app/services/socket-io.service.ts create mode 100644 src/app/shared/cities.ts create mode 100644 src/app/validators/city.validator.ts diff --git a/README.md b/README.md index a936ebf..e2350ca 100644 --- a/README.md +++ b/README.md @@ -62,17 +62,11 @@ Angular CLI does not come with an end-to-end testing framework by default. You c For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. --fix picture upload and adjust picture scale -WIP --implement websockets +-implement total visits and total clicks on register button --implement graph --implement total visits --search and filters (by name, city, age, etc.) --navigation between candidates (next/previous) (signals) wip change info make signals --edit page --implement live update + +minor things +-centralize scss check the same styles for differnet buttons -adjust to mobile format - - ****animations transitions (prev next cards) diff --git a/angular.json b/angular.json index 7ea7792..516489d 100644 --- a/angular.json +++ b/angular.json @@ -33,6 +33,7 @@ ], "styles": [ "@angular/material/prebuilt-themes/magenta-violet.css", + "node_modules/leaflet/dist/leaflet.css", "src/styles.scss" ], "scripts": [] @@ -121,4 +122,4 @@ } } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 044b5e8..f85006a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,14 @@ "@angular/platform-browser": "^19.1.0", "@angular/platform-browser-dynamic": "^19.1.0", "@angular/router": "^19.1.0", + "@maptiler/leaflet-maptilersdk": "^4.1.1", + "@swimlane/ngx-charts": "^23.0.1", + "chart.js": "^4.5.0", + "d3": "^7.9.0", + "leaflet": "^1.9.4", + "ng2-charts": "^8.0.0", "rxjs": "~7.8.0", + "socket.io-client": "^4.8.1", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, @@ -26,7 +33,9 @@ "@angular-devkit/build-angular": "^19.1.6", "@angular/cli": "^19.1.6", "@angular/compiler-cli": "^19.1.0", + "@types/geojson": "^7946.0.16", "@types/jasmine": "~5.1.0", + "@types/leaflet": "^1.9.20", "jasmine-core": "~5.5.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -3517,6 +3526,12 @@ "tslib": "2" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -3647,6 +3662,147 @@ "win32" ] }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", + "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", + "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.1" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "23.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.3.0.tgz", + "integrity": "sha512-IGJtuBbaGzOUgODdBRg66p8stnwj9iDXkgbYKoYcNiiQmaez5WVRfXm4b03MCDwmZyX93csbfHFWEJJYHnn5oA==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.0.3.tgz", + "integrity": "sha512-YsW99BwnT+ukJRkseBcLuZHfITB4puJoxnqPVjo72rhW/TaawVYsgQHcqWLzTxqknttYoDpgyERzWSa/XrETdA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/vector-tile": "^2.0.4", + "@types/geojson-vt": "3.2.5", + "@types/supercluster": "^7.1.3", + "geojson-vt": "^4.0.2", + "pbf": "^4.0.1", + "supercluster": "^8.0.1" + } + }, + "node_modules/@maptiler/client": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@maptiler/client/-/client-2.5.0.tgz", + "integrity": "sha512-MokjrX2HRq0JzwifLZM/7L36VuLGvTeTTLG5Kso4CAbZotPtIfk9IxidC9L4wrUaqxbDw7LyI1KkVq5kFE3GEg==", + "license": "BSD-3-Clause", + "dependencies": { + "quick-lru": "^7.0.0" + } + }, + "node_modules/@maptiler/leaflet-maptilersdk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@maptiler/leaflet-maptilersdk/-/leaflet-maptilersdk-4.1.1.tgz", + "integrity": "sha512-vCE0K9mlVRyAJkWYOngiA1ZEI42eoWvZd21HLYZE/IU1T+OQc4K1bPoap/z0myAXDLxIJ4wlz+vchr5tKgo0uA==", + "dependencies": { + "@maptiler/sdk": "^3.2.3", + "@types/leaflet": "^1.9.16", + "leaflet": "^1.9.4" + } + }, + "node_modules/@maptiler/sdk": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@maptiler/sdk/-/sdk-3.7.0.tgz", + "integrity": "sha512-zkh4iVqrPCV4S9M99mh3l7eX0mxbtHnoPCnxj/JfHuR7pw+SiCeQNrN8suHfT5ypGo1osnISPIzTtFGwW8pTqw==", + "license": "BSD-3-Clause", + "dependencies": { + "@maplibre/maplibre-gl-style-spec": "~23.3.0", + "@maptiler/client": "~2.5.0", + "events": "^3.3.0", + "gl-matrix": "^3.4.3", + "js-base64": "^3.7.7", + "maplibre-gl": "~5.6.0", + "uuid": "^11.0.5" + } + }, + "node_modules/@maptiler/sdk/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -5138,9 +5294,41 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true, "license": "MIT" }, + "node_modules/@swimlane/ngx-charts": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@swimlane/ngx-charts/-/ngx-charts-23.0.1.tgz", + "integrity": "sha512-mvdJTjBjUNoDj/R3AopYyVkVNrv0Zsrlh71CIQLVhBOs6PSV1mT3o7ukBFu8ct4w38wTfiGr8G+twHiYM1TEKA==", + "license": "MIT", + "dependencies": { + "d3-array": "^3.2.0", + "d3-brush": "^3.0.0", + "d3-color": "^3.1.0", + "d3-ease": "^3.0.1", + "d3-format": "^3.1.0", + "d3-hierarchy": "^3.1.2", + "d3-interpolate": "^3.0.1", + "d3-sankey": "^0.12.3", + "d3-scale": "^4.0.2", + "d3-selection": "^3.0.0", + "d3-shape": "^3.2.0", + "d3-time-format": "^4.1.0", + "d3-transition": "^3.0.1", + "gradient-path": "^2.3.0", + "tslib": "^2.3.1" + }, + "peerDependencies": { + "@angular/animations": "18.x || 19.x || 20.x", + "@angular/cdk": "18.x || 19.x || 20.x", + "@angular/common": "18.x || 19.x || 20.x", + "@angular/core": "18.x || 19.x || 20.x", + "@angular/forms": "18.x || 19.x || 20.x", + "@angular/platform-browser": "18.x || 19.x || 20.x", + "@angular/platform-browser-dynamic": "18.x || 19.x || 20.x", + "rxjs": "7.x" + } + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -5298,6 +5486,21 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -5329,6 +5532,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.20", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz", + "integrity": "sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -5420,6 +5632,21 @@ "@types/node": "*" } }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -6482,6 +6709,18 @@ "dev": true, "license": "MIT" }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -7114,6 +7353,456 @@ "dev": true, "license": "MIT" }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -7198,6 +7887,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7344,6 +8042,12 @@ "node": ">= 0.4" } }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7424,11 +8128,40 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -7738,7 +8471,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -8221,6 +8953,12 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -8283,6 +9021,24 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8366,6 +9122,15 @@ "dev": true, "license": "ISC" }, + "node_modules/gradient-path": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gradient-path/-/gradient-path-2.3.0.tgz", + "integrity": "sha512-vZdF/Z0EpqUztzWXFjFC16lqcialHacYoRonslk/bC6CuujkuIrqx7etlzdYHY4SnUU94LRWESamZKfkGh7yYQ==", + "license": "MIT", + "dependencies": { + "tinygradient": "^1.0.0" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -8654,7 +9419,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -8823,6 +9587,15 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ip-address": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", @@ -9243,6 +10016,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9293,6 +10072,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -9660,6 +10445,12 @@ "node": ">=10" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -9681,6 +10472,12 @@ "shell-quote": "^1.8.3" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/less": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", @@ -9937,6 +10734,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -10137,6 +10940,43 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/maplibre-gl": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.6.2.tgz", + "integrity": "sha512-SEqYThhUCFf6Lm0TckpgpKnto5u4JsdPYdFJb6g12VtuaFsm3nYdBO+fOmnUYddc8dXihgoGnuXvPPooUcRv5w==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.0.7", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^2.0.4", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^23.3.0", + "@maplibre/vt-pbf": "^4.0.3", + "@types/geojson": "^7946.0.16", + "@types/geojson-vt": "3.2.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.2", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10346,7 +11186,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10532,7 +11371,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msgpackr": { @@ -10583,6 +11421,12 @@ "multicast-dns": "cli.js" } }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -10647,6 +11491,24 @@ "dev": true, "license": "MIT" }, + "node_modules/ng2-charts": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-8.0.0.tgz", + "integrity": "sha512-nofsNHI2Zt+EAwT+BJBVg0kgOhNo9ukO4CxULlaIi7VwZSr7I1km38kWSoU41Oq6os6qqIh5srnL+CcV+RFPFA==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.15", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": ">=19.0.0", + "@angular/common": ">=19.0.0", + "@angular/core": ">=19.0.0", + "@angular/platform-browser": ">=19.0.0", + "chart.js": "^3.4.0 || ^4.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -11450,6 +12312,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -11659,6 +12533,12 @@ "dev": true, "license": "MIT" }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, "node_modules/proc-log": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", @@ -11690,6 +12570,12 @@ "node": ">=10" } }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -11776,6 +12662,24 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.1.0.tgz", + "integrity": "sha512-Pzd/4IFnTb8E+I1P5rbLQoqpUHcXKg48qTYKi4EANg+sTPwGFEMOcYGiiZz6xuQcOMZP7MPsrdAPx+16Q8qahg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -12004,6 +12908,15 @@ "node": ">=4" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, "node_modules/resolve-url-loader": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", @@ -12108,6 +13021,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.34.8", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", @@ -12184,6 +13103,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -12236,7 +13161,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, "license": "MIT" }, "node_modules/sass": { @@ -12823,11 +13747,42 @@ } } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dev": true, "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -12841,7 +13796,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -13207,6 +14161,15 @@ "node": ">=8" } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13432,6 +14395,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -13449,6 +14418,22 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinygradient": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", + "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", + "license": "MIT", + "dependencies": { + "@types/tinycolor2": "^1.4.0", + "tinycolor2": "^1.0.0" + } + }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/tmp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", @@ -14781,7 +15766,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -14799,6 +15783,14 @@ } } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index dc542af..aaf7881 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "ng": "ng", "start": "ng serve", - "start:remote-api":"ng serve --configuration=remote-api", + "start:remote-api": "ng serve --configuration=remote-api", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test" @@ -21,7 +21,14 @@ "@angular/platform-browser": "^19.1.0", "@angular/platform-browser-dynamic": "^19.1.0", "@angular/router": "^19.1.0", + "@maptiler/leaflet-maptilersdk": "^4.1.1", + "@swimlane/ngx-charts": "^23.0.1", + "chart.js": "^4.5.0", + "d3": "^7.9.0", + "leaflet": "^1.9.4", + "ng2-charts": "^8.0.0", "rxjs": "~7.8.0", + "socket.io-client": "^4.8.1", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, @@ -29,7 +36,9 @@ "@angular-devkit/build-angular": "^19.1.6", "@angular/cli": "^19.1.6", "@angular/compiler-cli": "^19.1.0", + "@types/geojson": "^7946.0.16", "@types/jasmine": "~5.1.0", + "@types/leaflet": "^1.9.20", "jasmine-core": "~5.5.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -38,4 +47,4 @@ "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.7.2" } -} \ No newline at end of file +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7c7babe..62f5fea 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import { Component, inject } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; import { RouterOutlet } from '@angular/router'; @Component({ @@ -8,5 +8,4 @@ import { RouterOutlet } from '@angular/router'; templateUrl: './app.component.html', styleUrl: './app.component.scss' }) -export class AppComponent { -} +export class AppComponent { } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index f436384..db09f36 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,15 +1,17 @@ import { Routes } from '@angular/router'; -import { RegistrationComponent } from './registration/registration.component'; -import { LandingComponent } from './landing/landing.component'; -import { ApplicationListComponent } from './application-list/application-list.component'; -import { ApplicationComponent } from './application/application.component'; +import { RegistrationComponent } from './components/registration/registration.component'; +import { LandingComponent } from './components/landing/landing.component'; +import { ApplicationListComponent } from './components/application-list/application-list.component'; +import { ApplicationComponent } from './components/application/application.component'; +import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; export const routes: Routes = [ { path: 'landing', component: LandingComponent }, - { path: 'registration', component: RegistrationComponent }, + { path: 'application/new', component: RegistrationComponent }, + { path: 'application/:id/edit', component: RegistrationComponent }, { path: 'application-list', component: ApplicationListComponent }, { path: 'application/:id', component: ApplicationComponent }, { path: '', redirectTo: '/landing', pathMatch: 'full' }, - { path: '**', redirectTo: '/landing' }, - // component: PageNotFoundComponent + { path: '**', component: PageNotFoundComponent }, ]; + diff --git a/src/app/application-list/application-list.component.html b/src/app/application-list/application-list.component.html deleted file mode 100644 index 2ce37fd..0000000 --- a/src/app/application-list/application-list.component.html +++ /dev/null @@ -1,5 +0,0 @@ -@for(application of this.applicationList; track application.id){ -
-

{{application.fullName}}

-
-} \ No newline at end of file diff --git a/src/app/application-list/application-list.component.scss b/src/app/application-list/application-list.component.scss deleted file mode 100644 index aa84483..0000000 --- a/src/app/application-list/application-list.component.scss +++ /dev/null @@ -1,9 +0,0 @@ -.listItem { - margin: 2rem; - background-color: aquamarine; - padding: 1rem; - &:hover{ - cursor: pointer; - background-color: aqua; - } -} \ No newline at end of file diff --git a/src/app/application-list/application-list.component.ts b/src/app/application-list/application-list.component.ts deleted file mode 100644 index db74ea0..0000000 --- a/src/app/application-list/application-list.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component, inject, OnInit } from '@angular/core'; -import { CandidateDataService } from '../candidate-data.service'; -import { RouterLink } from '@angular/router'; - -@Component({ - selector: 'app-application-list', - imports: [RouterLink], - templateUrl: './application-list.component.html', - styleUrl: './application-list.component.scss' -}) -export class ApplicationListComponent implements OnInit { - - dataService = inject(CandidateDataService); - applicationList: any[] = []; - - ngOnInit(): void { - this.dataService.loadCandidateList().subscribe((data) => { - this.applicationList = data; - }); - - } - -} diff --git a/src/app/application/application.component.html b/src/app/application/application.component.html deleted file mode 100644 index a94ee3c..0000000 --- a/src/app/application/application.component.html +++ /dev/null @@ -1 +0,0 @@ -{{this.application}} \ No newline at end of file diff --git a/src/app/application/application.component.scss b/src/app/application/application.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/application/application.component.ts b/src/app/application/application.component.ts deleted file mode 100644 index 44fb9c7..0000000 --- a/src/app/application/application.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component, inject, OnInit } from '@angular/core'; -import { CandidateDataService } from '../candidate-data.service'; -import { ActivatedRoute, RouterModule } from '@angular/router'; - -@Component({ - selector: 'app-application', - imports: [RouterModule], - templateUrl: './application.component.html', - styleUrl: './application.component.scss' -}) -export class ApplicationComponent implements OnInit { - dataService = inject(CandidateDataService); - activatedRoute = inject(ActivatedRoute); - application: any = {}; - - ngOnInit(): void { - const id = this.activatedRoute.snapshot.paramMap.get('id'); - if (!id) { - alert('invalid route'); - return; - } - const applicationId = Number.parseInt(id, 10); - this.dataService.getApplicationDetails(applicationId).subscribe((data) => { - this.application = JSON.stringify(data); - }) - } - -} diff --git a/src/app/candidate-data.service.ts b/src/app/candidate-data.service.ts deleted file mode 100644 index 17e33ad..0000000 --- a/src/app/candidate-data.service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { HttpClient } from "@angular/common/http"; -import { inject, Injectable } from "@angular/core"; -import { environment } from "../environments/environment.development"; - -@Injectable({ - providedIn: 'root' -}) -export class CandidateDataService { - httpClient = inject(HttpClient) - - - loadCandidateList() { - return this.httpClient.get(`${environment.hostUrl}/app/candidates`); - } - getApplicationDetails(id: number) { - return this.httpClient.get(`${environment.hostUrl}/app/candidate/${id}`) - } - submitCandidateForm(formData: Object) { - return this.httpClient.post(`${environment.hostUrl}/app/register`, formData); - } - - -} \ No newline at end of file diff --git a/src/app/components/application-list/application-list.component.html b/src/app/components/application-list/application-list.component.html new file mode 100644 index 0000000..4bfee01 --- /dev/null +++ b/src/app/components/application-list/application-list.component.html @@ -0,0 +1,99 @@ +
+
+
+ +

Applications

+
+ + @if (!isLoading) { +
+
+

Age Distribution

+ + +
+ +
+

City Distribution

+ + +
+
+ } + + @if (!isLoading) { +
+ + Search + + + + + City + + All + @for (city of availableCities(); track city.name) { + {{ city.name }} + } + + + + + Sort by + + @for (option of sortFields; track option.value) { + + {{ option.viewValue }} + + } + + +
+ } +
+ + @if (isLoading) { +
+ +

Loading applications...

+
+ } @else { +
+ @for (application of sortedList(); track application.id) { +
+ @if (application.profileImage) { + {{ application.fullName }} + } +
+

{{ application.fullName }}

+

Age: {{ application.age }}

+

City: {{ application.cityOrRegion }}

+
+
+ } +
+ } +
diff --git a/src/app/components/application-list/application-list.component.scss b/src/app/components/application-list/application-list.component.scss new file mode 100644 index 0000000..94ece74 --- /dev/null +++ b/src/app/components/application-list/application-list.component.scss @@ -0,0 +1,187 @@ +/* Fullscreen container */ +.container { + width: 100%; + min-height: 100vh; + margin: 0; + padding: 2rem; + background: radial-gradient(circle at top, #0a0f2c 0%, #000000 100%); + color: #e0e0ff; + font-family: 'Orbitron', Arial, sans-serif; + box-sizing: border-box; +} + +/* Header row */ +.list-header-top { + display: flex; + align-items: center; + justify-content: center; + position: relative; + margin-bottom: 2rem; +} + +.back-button { + position: absolute; + left: 0; + background: transparent; + color: #00ffff; + border: 1px solid #00ffff; + border-radius: 8px; + padding: 0.5rem 1rem; + font-weight: 700; + cursor: pointer; + transition: all 0.3s ease; +} + +.back-button:hover { + background: rgba(0, 255, 255, 0.12); + box-shadow: 0 0 20px #00ffff; + transform: translateY(-2px); +} + +/* Shining header */ +.page-title { + text-align: center; + font-weight: 700; + background: linear-gradient(90deg, #00ffff, #ffffff, #00ffff); + background-size: 200% auto; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + -webkit-text-fill-color: transparent; + animation: shine 3s linear infinite; + text-shadow: 0 0 12px rgba(0, 255, 255, 0.6); +} + +/* Filters row centered */ +.list-header { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 2rem; +} + +/* Fields sizing */ +.search-field, .filter-field, .sort-field { + width: 180px; +} + +.sort-field { + width: 150px; +} + +/* Applications list */ +.app-list .list { + display: flex; + flex-direction: column; + gap: 1.2rem; + max-width: 900px; + margin: 0 auto; +} + + +.profile-photo { + width: 100px; + height: 100px; + border-radius: 10%; + object-fit: cover; + box-shadow: 0 0 12px rgba(0, 255, 255, 0.5); + flex-shrink: 0; +} + +/* Applicant card layout */ +.listItem { + display: flex; + align-items: center; + gap: 1.2rem; + padding: 1rem 1.4rem; + border: 1px solid #00ffff; + border-radius: 14px; + background: rgba(255, 255, 255, 0.03); + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 0 12px rgba(0, 255, 255, 0.15); +} + +.listItem:hover { + background: rgba(0, 255, 255, 0.08); + box-shadow: 0 0 22px #00ffff; + transform: translateY(-3px); +} + +/* Applicant text info */ +.listItem .info { + display: flex; + flex-direction: column; + justify-content: center; +} + +.listItem p { + margin: 0.15rem 0; + font-size: 1rem; + font-weight: 500; + color: #e0e0ff; + text-shadow: 0 0 6px rgba(0, 255, 255, 0.6); +} + +.app-name { + font-size: 1.2rem; + font-weight: 600; + color: #00ffff; +} + + +.charts-row { + display: flex; + justify-content: center; + gap: 2rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.chart-container { + width: 300px; + background: rgba(0, 0, 0, 0.3); + padding: 1.2rem; + border-radius: 14px; + text-align: center; + box-shadow: 0 0 18px rgba(0, 255, 255, 0.15); + border: 1px solid rgba(0, 255, 255, 0.3); + +} + +.chart-container h3 { + margin-bottom: 1rem; + text-shadow: 0 0 6px rgba(0, 255, 255, 0.5); + font-weight: 600; + font-size: 1.2rem; + color: #00ffff; + text-shadow: 0 0 6px rgba(0, 255, 255, 0.6); +} + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80px 20px; + text-align: center; + min-height: 400px; + + .loading-text { + margin-top: 24px; + font-size: 1.1rem; + color: #e0e0ff; + font-weight: 600; + font-family: 'Orbitron', Arial, sans-serif; + text-shadow: 0 0 6px rgba(0, 255, 255, 0.6); + animation: pulse 2s ease-in-out infinite alternate; + } +} + +/* Shining animation */ +@keyframes shine { + 0% { background-position: -200% center; } + 100% { background-position: 200% center; } +} diff --git a/src/app/components/application-list/application-list.component.ts b/src/app/components/application-list/application-list.component.ts new file mode 100644 index 0000000..b5d00b0 --- /dev/null +++ b/src/app/components/application-list/application-list.component.ts @@ -0,0 +1,212 @@ +import { Component, OnInit, inject, signal, computed, effect } from '@angular/core'; +import { CandidateDataService } from '../../services/candidate-data.service'; +import { Router, RouterLink } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { City, CITY_LIST } from '../../shared/cities'; +import { environment } from '../../../environments/environment.development'; +import { SocketIOService } from '../../services/socket-io.service'; +import { BaseChartDirective } from 'ng2-charts'; +import { ChartData, ChartOptions, Chart, registerables } from 'chart.js'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +Chart.register(...registerables); + +@Component({ + selector: 'app-application-list', + standalone: true, + imports: [ + RouterLink, + FormsModule, + MatFormFieldModule, + MatSelectModule, + MatIconModule, + MatInputModule, + MatProgressSpinnerModule, + BaseChartDirective, + ], + templateUrl: './application-list.component.html', + styleUrls: ['./application-list.component.scss'] +}) +export class ApplicationListComponent implements OnInit { + private dataService = inject(CandidateDataService); + private socketService = inject(SocketIOService); + private router = inject(Router); + + environment = environment; + + applicationList = signal([]); + searchTerm = signal(''); + filterCity = signal(''); + sortField = signal('fullName'); + + availableCities = signal(CITY_LIST); + + filteredList = computed(() => { + const apps = this.applicationList(); + const term = this.searchTerm().toLowerCase(); + const city = this.filterCity(); + + return apps.filter(app => { + const matchesSearch = term + ? app.fullName?.toLowerCase().includes(term) || + app.cityOrRegion?.toLowerCase().includes(term) + : true; + + const matchesCity = city ? app.cityOrRegion === city : true; + return matchesSearch && matchesCity; + }); + }); + + sortedList = computed(() => { + const field = this.sortField(); + return [...this.filteredList()].sort((a, b) => { + let valueA = a[field]; + let valueB = b[field]; + + if (typeof valueA === 'string' && typeof valueB === 'string') { + return valueA.toLowerCase().localeCompare(valueB.toLowerCase()); + } + return valueA < valueB ? -1 : valueA > valueB ? 1 : 0; + }); + }); + + ageChartData = signal>({ labels: [], datasets: [] }); + cityChartData = signal>({ labels: [], datasets: [] }); + + chartOptions: ChartOptions<'pie'> = { + responsive: true, + plugins: { + legend: { position: 'bottom', labels: { color: '#e0e0ff' } } + } + }; + + sortFields = [ + { value: 'fullName', viewValue: 'Name' }, + { value: 'age', viewValue: 'Age' }, + { value: 'date', viewValue: 'Date' }, + { value: 'cityOrRegion', viewValue: 'City' } + ]; + + constructor() { + effect(() => { + const data = this.applicationList(); + + if (data.length === 0) { + this.ageChartData.set({ labels: [], datasets: [] }); + this.cityChartData.set({ labels: [], datasets: [] }); + return; + } + + const ageGroups: Record = { + '18-25': 0, + '26-35': 0, + '36-45': 0, + '46-60': 0, + '60+': 0, + }; + + data.forEach(app => { + if (app.age <= 25) ageGroups['18-25']++; + else if (app.age <= 35) ageGroups['26-35']++; + else if (app.age <= 45) ageGroups['36-45']++; + else if (app.age <= 60) ageGroups['46-60']++; + else ageGroups['60+']++; + }); + + this.ageChartData.set({ + labels: Object.keys(ageGroups), + datasets: [{ + data: Object.values(ageGroups), + backgroundColor: [ + 'rgba(0, 255, 255, 0.6)', + 'rgba(0, 200, 255, 0.6)', + 'rgba(170, 120, 255, 0.6)', + 'rgba(255, 180, 80, 0.6)', + 'rgba(255, 80, 120, 0.6)', + ], + borderColor: '#00ffff', + borderWidth: 1, + }] + }); + + const cityCounts: Record = {}; + data.forEach(app => { + const city = app.cityOrRegion || 'Unknown'; + cityCounts[city] = (cityCounts[city] || 0) + 1; + }); + + const sortedCities = Object.entries(cityCounts).sort((a, b) => b[1] - a[1]); + const topCities = sortedCities.slice(0, 6); + const others = sortedCities.slice(6); + + const labels = topCities.map(([city]) => city); + const values = topCities.map(([_, count]) => count); + + if (others.length) { + labels.push('Others'); + values.push(others.reduce((sum, [_, count]) => sum + count, 0)); + } + + this.cityChartData.set({ + labels, + datasets: [{ + data: values, + backgroundColor: [ + 'rgba(0, 255, 255, 0.6)', + 'rgba(0, 200, 255, 0.6)', + 'rgba(170, 120, 255, 0.6)', + 'rgba(255, 180, 80, 0.6)', + 'rgba(255, 80, 120, 0.6)', + 'rgba(80, 255, 150, 0.6)', + 'rgba(120, 120, 120, 0.6)', + ], + borderColor: '#00ffff', + borderWidth: 1, + }] + }); + }); + } + + get isLoading() { + return this.dataService.isCandidatesListLoading(); + } + + ngOnInit(): void { + this.dataService.loadCandidateList().subscribe(data => { + this.applicationList.set(data); + this.availableCities.set(this.getUniqueCities(data)); + }); + + this.socketService.onCandidateRegistered().subscribe(newCandidate => { + this.applicationList.update(list => [newCandidate, ...list]); + this.availableCities.set(this.getUniqueCities(this.applicationList())); + }); + + this.socketService.onCandidateUpdated().subscribe(updatedCandidate => { + this.applicationList.update(list => + list.map(app => app.id === updatedCandidate.id ? updatedCandidate : app) + ); + this.availableCities.set(this.getUniqueCities(this.applicationList())); + }); + } + + goBack(): void { + this.router.navigate(['/landing']); + } + + getUniqueCities(data: any[]): City[] { + const seen: string[] = []; + return data + .map(app => app.cityOrRegion) + .filter(city => city && !seen.includes(city) && seen.push(city)) + .map(cityName => { + const found = CITY_LIST.find(c => c.name === cityName); + return found ?? { name: cityName, lat: 0, lng: 0 }; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + } +} \ No newline at end of file diff --git a/src/app/components/application/application.component.html b/src/app/components/application/application.component.html new file mode 100644 index 0000000..87fad1e --- /dev/null +++ b/src/app/components/application/application.component.html @@ -0,0 +1,150 @@ +
+
+ +

🛰 Application Details

+
+ + @if (isApplicationDetailsLoading) { +
+ +

Loading application details...

+
+ } @else if (hasApplicationData) { +
+ + + + + +
+ + +
+ @if (currentApplication().profileImage) { +
+ +
+ } + +
+

{{ currentApplication().fullName }}

+
+
+ person + {{ currentApplication().age }} years old +
+
+ location_on + {{ currentApplication().cityOrRegion }} +
+ @if (currentApplication().email) { +
+ email + {{ currentApplication().email }} +
+ } + @if (currentApplication().phoneNumber) { +
+ phone + {{ currentApplication().phoneNumber }} +
+ } +
+
+
+ + +
+ @if (getHobbiesArray().length > 0) { +
+

favorite Hobbies

+
+ @for (hobby of getHobbiesArray(); track hobby) { + {{ hobby }} + } +
+
+ } + + @if (currentApplication().justification) { +
+

rocket_launch Journey Justification?

+

{{ currentApplication().justification }}

+
+ } + + @if (currentApplication().createdAt) { +
+

calendar_today Application Date

+

+ {{ formatDate(currentApplication().createdAt) }} +

+
+ } +
+ + +
+ @if (canEdit()) { + + } @else { +

You can edit your application only during 3 days

+ } +
+ + +
+ View Raw Data +
{{ currentApplication() | json }}
+
+
+
+ } @else { +
+ inbox +

No application data available

+ +
+ } +
diff --git a/src/app/components/application/application.component.scss b/src/app/components/application/application.component.scss new file mode 100644 index 0000000..498b356 --- /dev/null +++ b/src/app/components/application/application.component.scss @@ -0,0 +1,417 @@ +.container { + width: 100%; + min-height: 100vh; + margin: 0; + padding: 1rem; + background: radial-gradient(circle at top, #0a0f2c 0%, #000000 100%); + color: #e0e0ff; + font-family: 'Orbitron', Arial, sans-serif; + box-sizing: border-box; + + @media (min-width: 768px) { + padding: 2rem; + } +} + +/* Shining text effect */ +@keyframes shine { + 0% { background-position: -200% center; } + 100% { background-position: 200% center; } +} + +@keyframes pulse { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +/* Header */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + flex-wrap: wrap; + gap: 1rem; + + h2 { + margin: 0; + font-weight: 700; + background: linear-gradient(90deg, #00ffff, #ffffff, #00ffff); + background-size: 200% auto; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: shine 3s linear infinite; + text-shadow: 0 0 12px rgba(0, 255, 255, 0.6); + font-size: clamp(1.5rem, 4vw, 2rem); + } +} + +/* Content wrapper */ +.content-wrapper { + max-width: 1200px; + margin: 0 auto; +} + +/* Navigation Controls */ +.navigation-controls { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 2rem; + gap: 2rem; + padding: 1rem; + background: rgba(0, 0, 0, 0.3); + border-radius: 12px; + backdrop-filter: blur(10px); + border: 1px solid rgba(0, 255, 255, 0.2); + + .navigation-info { + font-size: 1.1rem; + font-weight: 600; + color: #00ffff; + text-shadow: 0 0 8px rgba(0, 255, 255, 0.4); + min-width: 80px; + text-align: center; + } +} + +.nav-button { + background: rgba(0, 255, 255, 0.1); + border: 2px solid #00ffff; + border-radius: 50%; + color: #00ffff; + transition: all 0.3s ease; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + + &:hover:not(:disabled) { + background: rgba(0, 255, 255, 0.3); + transform: scale(1.1); + box-shadow: 0 0 20px rgba(0, 255, 255, 0.5); + animation: float 2s ease-in-out infinite; + } + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + border-color: rgba(0, 255, 255, 0.3); + } + + mat-icon { + font-size: 24px; + width: 24px; + height: 24px; + } +} + +/* Application Card */ +.application-card { + background: rgba(0, 0, 0, 0.4); + border-radius: 16px; + padding: 2rem; + backdrop-filter: blur(15px); + border: 1px solid rgba(0, 255, 255, 0.2); + box-shadow: 0 8px 32px rgba(0, 255, 255, 0.1); + margin-bottom: 2rem; +} + +/* Profile Section */ +.profile-section { + display: flex; + gap: 2rem; + margin-bottom: 3rem; + padding-bottom: 2rem; + border-bottom: 2px solid rgba(0, 255, 255, 0.2); + align-items: center; + flex-wrap: wrap; + + @media (max-width: 768px) { + flex-direction: column; + text-align: center; + gap: 1rem; + } +} + +.profile-image-container { + flex-shrink: 0; + + .profile-image { + width: 120px; + height: 120px; + border-radius: 50%; + object-fit: cover; + border: 3px solid #00ffff; + box-shadow: 0 0 20px rgba(0, 255, 255, 0.4); + transition: transform 0.3s ease; + + &:hover { + transform: scale(1.05); + box-shadow: 0 0 30px rgba(0, 255, 255, 0.6); + } + + @media (max-width: 768px) { + width: 100px; + height: 100px; + } + } +} + +.profile-info { + flex: 1; + + .candidate-name { + font-size: 2rem; + font-weight: 700; + margin: 0 0 1rem 0; + background: linear-gradient(135deg, #00ffff, #ffffff); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0 0 10px rgba(0, 255, 255, 0.3); + + @media (max-width: 768px) { + font-size: 1.5rem; + } + } + + .basic-info { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .info-item { + display: flex; + align-items: center; + gap: 0.75rem; + color: #e0e0ff; + font-size: 1rem; + + mat-icon { + color: #00ffff; + font-size: 20px; + width: 20px; + height: 20px; + } + } +} + +/* Details Grid */ +.details-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-bottom: 2rem; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + gap: 1.5rem; + } +} + +.detail-section { + background: rgba(0, 255, 255, 0.05); + border-radius: 12px; + padding: 1.5rem; + border: 1px solid rgba(0, 255, 255, 0.2); + transition: all 0.3s ease; + + &:hover { + background: rgba(0, 255, 255, 0.1); + border-color: rgba(0, 255, 255, 0.4); + transform: translateY(-2px); + } + + &.full-width { + grid-column: 1 / -1; + } + + h4 { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0 1rem 0; + color: #00ffff; + font-size: 1.2rem; + font-weight: 600; + + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + } + } + + .detail-content { + margin: 0; + line-height: 1.6; + color: #e0e0ff; + } +} + +/* Hobbies */ +.hobbies-container { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.hobby-chip { + background: rgba(255, 0, 150, 0.2); + color: #ff0096; + border: 1px solid rgba(255, 0, 150, 0.4); + font-weight: 500; + font-size: 0.9rem; + transition: all 0.3s ease; + + &:hover { + background: rgba(255, 0, 150, 0.3); + transform: scale(1.05); + } +} + +/* Buttons */ +.back-button { + background: transparent; + color: #00ffff; + border: 2px solid #00ffff; + border-radius: 8px; + padding: 0.75rem 1.5rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; + transition: all 0.3s ease; + + &:hover { + background: rgba(0, 255, 255, 0.1); + transform: translateY(-2px); + box-shadow: 0 4px 20px rgba(0, 255, 255, 0.3); + } + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } +} + +/* Loading */ +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + min-height: 400px; + + .loading-text { + margin-top: 1.5rem; + font-size: 1.1rem; + color: #e0e0ff; + font-weight: 600; + text-shadow: 0 0 6px rgba(0, 255, 255, 0.6); + animation: pulse 2s ease-in-out infinite; + } +} + +/* No Data State */ +.no-data { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + min-height: 400px; + + .no-data-icon { + font-size: 4rem; + width: 4rem; + height: 4rem; + color: rgba(0, 255, 255, 0.4); + margin-bottom: 1rem; + } + + p { + font-size: 1.2rem; + color: #e0e0ff; + margin-bottom: 2rem; + } +} + +/* Raw Data Section */ +.raw-data-section { + margin-top: 2rem; + border-top: 2px solid rgba(0, 255, 255, 0.2); + padding-top: 2rem; + + summary { + color: #00ffff; + cursor: pointer; + font-weight: 600; + padding: 0.5rem 0; + + &:hover { + text-shadow: 0 0 8px rgba(0, 255, 255, 0.6); + } + } + + .raw-data { + margin-top: 1rem; + padding: 1rem; + background: rgba(0, 0, 0, 0.5); + border-radius: 8px; + font-family: 'Courier New', monospace; + font-size: 0.85rem; + line-height: 1.4; + color: #00ffff; + border: 1px solid rgba(0, 255, 255, 0.2); + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + } +}.edit-controls { + display: flex; + justify-content: flex-end; + margin-top: 2rem; + + .edit-button { + background: #00ffff; + color: #000; + font-weight: 600; + padding: 0.75rem 1.5rem; + border-radius: 8px; + box-shadow: 0 0 12px rgba(0, 255, 255, 0.3); + transition: all 0.3s ease; + + display: flex; + align-items: center; + gap: 0.5rem; + + &:hover { + box-shadow: 0 0 20px #00ffff; + transform: translateY(-2px); + } + + mat-icon { + font-size: 20px; + } + } + + .edit-info { + color: #aaaaff; + font-size: 0.95rem; + font-style: italic; + } +} \ No newline at end of file diff --git a/src/app/components/application/application.component.ts b/src/app/components/application/application.component.ts new file mode 100644 index 0000000..b2b5aba --- /dev/null +++ b/src/app/components/application/application.component.ts @@ -0,0 +1,170 @@ +import { Component, inject, OnInit, signal, computed } from '@angular/core'; +import { CandidateDataService } from '../../services/candidate-data.service'; +import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatCardModule } from '@angular/material/card'; +import { MatChipsModule } from '@angular/material/chips'; +import { CommonModule } from '@angular/common'; +import { environment } from '../../../environments/environment'; +import { SocketIOService } from '../../services/socket-io.service'; + +@Component({ + selector: 'app-application', + imports: [ + RouterModule, + MatIconModule, + MatButtonModule, + MatProgressSpinnerModule, + MatCardModule, + MatChipsModule, + CommonModule + ], + templateUrl: './application.component.html', + styleUrls: ['./application.component.scss'] +}) +export class ApplicationComponent implements OnInit{ + dataService = inject(CandidateDataService); + socketService = inject(SocketIOService); + activatedRoute = inject(ActivatedRoute); + router = inject(Router); + + currentApplication = signal(null); + applicationList = signal([]); + currentIndex = signal(-1); + environment = environment; + + get isApplicationDetailsLoading() { + return this.dataService.isApplicationDetailsLoading(); + } + + get hasApplicationData() { + return this.currentApplication() !== null; + } + + canGoToPrevious = computed(() => this.currentIndex() > 0); + canGoToNext = computed(() => + this.currentIndex() >= 0 && + this.currentIndex() < this.applicationList().length - 1 + ); + + canEdit = computed(() => { + const app = this.currentApplication(); + if (!app?.createdAt) return false; + const created = new Date(app.createdAt); + const now = new Date(); + return now.getTime() - created.getTime() <= 3 * 24 * 60 * 60 * 1000; + }); + + + ngOnInit(): void { + if (this.dataService.cachedApplicationList.length > 0) { + this.applicationList.set(this.dataService.cachedApplicationList); + this.initializeApplication(); + } else { + this.dataService.loadCandidateList().subscribe({ + next: (data) => { + this.applicationList.set(data); + this.initializeApplication(); + }, + error: (error) => { + console.error('Error loading candidate list:', error); + alert('Error loading candidate list'); + } + }); + } + } + + editApplication(): void { + const app = this.currentApplication(); + if (!app) return; + this.router.navigate(['/application', app.id, 'edit']); + } + + initializeApplication(): void { + const id = this.activatedRoute.snapshot.paramMap.get('id'); + if (!id) { + alert('Invalid route'); + return; + } + + const applicationId = Number.parseInt(id, 10); + const foundIndex = this.applicationList().findIndex(app => app.id === applicationId); + + if (foundIndex === -1) { + alert('Application not found'); + return; + } + + if (this.currentIndex() !== foundIndex) { + this.currentIndex.set(foundIndex); + this.loadApplication(applicationId); + } + } + + loadApplication(id: number): void { + if (this.currentApplication()?.id === id) { + return; + } + + this.currentApplication.set(null); + this.dataService.getApplicationDetails(id).subscribe({ + next: (data) => { + this.currentApplication.set(data); + const currentRoute = this.router.url; + const targetRoute = `/application/${id}`; + if (currentRoute !== targetRoute) { + this.router.navigate(['/application', id], { replaceUrl: true }); + } + }, + error: (error) => { + console.error('Error loading application details:', error); + alert('Error loading application details'); + } + }); + } + + goToPrevious(): void { + if (this.canGoToPrevious()) { + const newIndex = this.currentIndex() - 1; + this.currentIndex.set(newIndex); + const prevId = this.applicationList()[newIndex].id; + this.loadApplication(prevId); + } + } + + goToNext(): void { + if (this.canGoToNext()) { + const newIndex = this.currentIndex() + 1; + this.currentIndex.set(newIndex); + const nextId = this.applicationList()[newIndex].id; + this.loadApplication(nextId); + } + } + + goBack(): void { + this.router.navigate(['/application-list']); + } + + getHobbiesArray(): string[] { + const app = this.currentApplication(); + if (!app || !app.hobbies) return []; + + if (typeof app.hobbies === 'string') { + return app.hobbies.split(',').map((hobby: string) => hobby.trim()).filter(Boolean); + } + return Array.isArray(app.hobbies) ? app.hobbies : []; + } + + formatDate(dateString: string): string { + if (!dateString) return 'Not specified'; + try { + return new Date(dateString).toLocaleDateString(); + } catch { + return dateString; + } + } + + +} \ No newline at end of file diff --git a/src/app/components/image-input/image-input.component.html b/src/app/components/image-input/image-input.component.html new file mode 100644 index 0000000..62a3ed5 --- /dev/null +++ b/src/app/components/image-input/image-input.component.html @@ -0,0 +1,40 @@ +
+ Profile Photo + +
+ + @if (value) { +
+ + +
+ } + + + @if (!value) { +
+ file_upload + + Drag and drop here +
+ } + + + +
+
diff --git a/src/app/components/image-input/image-input.component.scss b/src/app/components/image-input/image-input.component.scss new file mode 100644 index 0000000..6be75bd --- /dev/null +++ b/src/app/components/image-input/image-input.component.scss @@ -0,0 +1,75 @@ +.fileUploadContainer { + position: relative; + padding: 5px; + display: flex; + flex-direction: column; + margin: 0 auto; + width: 150px; + height: 150px; + border: 2px double hsla(180, 100%, 50%, 0.324); + text-align: center; + justify-content: center; + align-items: center; + overflow: hidden; + border-radius: 8px; + background-color: rgba(0, 15, 44, 0.8); + /* Preview image */ + .previewImage { + display: block; + width: 100%; + height: 100%; + object-fit: contain; + object-position: center; + border-radius: 0; + transition: transform 0.3s ease; + } + + + /* Upload prompt when no image */ + .uploadPrompt { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 11px; + color: #00ffff; + gap: 5px; + opacity: 0.7; + transition: opacity 0.3s; + } + + .uploadPrompt:hover { + opacity: 1; + } + + /* Delete button */ + .deleteButton { + position: absolute; + top: 5px; + right: 5px; + z-index: 10; + opacity: 0.7; + transition: opacity 0.2s, transform 0.2s; + color: #000; + background-color: rgba(0, 255, 255, 0.7); /* neon highlight */ + border-radius: 50%; + } + + .deleteButton:hover { + opacity: 1; + transform: scale(1.2); + background-color: rgba(0, 255, 255, 0.9); + } + + /* Hidden file input */ + .fileInput { + position: absolute; + z-index: 1; + opacity: 0; + height: 100%; + width: 100%; + top: 0; + left: 0; + cursor: pointer; + } +} diff --git a/src/app/components/image-input/image-input.component.ts b/src/app/components/image-input/image-input.component.ts new file mode 100644 index 0000000..3ee1a4e --- /dev/null +++ b/src/app/components/image-input/image-input.component.ts @@ -0,0 +1,78 @@ +import { Component, forwardRef, inject } from '@angular/core'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatIconModule } from '@angular/material/icon'; +import { CommonModule } from '@angular/common'; + + + + +@Component({ + selector: 'app-image-input', + imports: [ + CommonModule, + MatButtonModule, + MatIconModule, + MatFormFieldModule, + MatInputModule, + FormsModule, + ReactiveFormsModule + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ImageInputComponent), + multi: true + } + ], + templateUrl: './image-input.component.html', + styleUrl: './image-input.component.scss' +}) +export class ImageInputComponent implements ControlValueAccessor { + value: File | null = null; + disabled = false; + previewUrl: string | null = null; + + + onChange = (value: any) => { }; + onTouched = () => { }; + + writeValue(value: File | null): void { + this.value = value; + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + + setFileData(event: Event): void { + const input = event.target as HTMLInputElement; + if (input?.files?.[0]) { + this.value = input.files[0]; + + this.previewUrl = URL.createObjectURL(this.value); + this.onChange(this.value); + } + } + + clear() { + this.value = null; + if (this.previewUrl) { + URL.revokeObjectURL(this.previewUrl); + } + this.previewUrl = null; + this.onChange(this.value); + } +} + diff --git a/src/app/components/landing/landing.component.html b/src/app/components/landing/landing.component.html new file mode 100644 index 0000000..847aafe --- /dev/null +++ b/src/app/components/landing/landing.component.html @@ -0,0 +1,14 @@ + +
+

Welcome to IISA

+

Join the mission or track your application below.

+ +
+ + +
+
diff --git a/src/app/components/landing/landing.component.scss b/src/app/components/landing/landing.component.scss new file mode 100644 index 0000000..f6d02bc --- /dev/null +++ b/src/app/components/landing/landing.component.scss @@ -0,0 +1,115 @@ +@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap'); + +/* Fullscreen container */ +.container { + width: 100%; + min-height: 100vh; + margin: 0; + padding: 2rem; + background: radial-gradient(circle at top, #0a0f2c 0%, #000000 100%); + color: #e0e0ff; + font-family: 'Orbitron', Arial, sans-serif; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +/* Shining text animation */ +@keyframes shine { + 0% { + background-position: -200% center; + } + 100% { + background-position: 200% center; + } +} + +/* Shining header */ +.landing h2 { + font-weight: 700; + margin-bottom: 1.5rem; + font-size: 2rem; + background: linear-gradient(90deg, #00ffff, #ffffff, #00ffff); + background-size: 200% auto; + background-clip: text; /* Standard */ + -webkit-background-clip: text; /* Chrome/Safari */ + color: transparent; + -webkit-text-fill-color: transparent; + animation: shine 3s linear infinite; + text-shadow: 0 0 12px rgba(0, 255, 255, 0.6); +} + +/* Intro paragraph */ +.landing .intro { + font-size: 1.2rem; + color: #a0a0ff; + margin-bottom: 2.5rem; + text-shadow: 0 0 6px rgba(0, 255, 255, 0.3); +} + +/* Button group */ +.button-group { + display: flex; + gap: 1.5rem; + flex-wrap: wrap; + justify-content: center; +} + +/* Primary button */ +button.primary-btn { + background: #00ffff; + color: #000; + padding: 0.8rem 1.5rem; + border-radius: 8px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 0 12px rgba(0, 255, 255, 0.3); +} + +button.primary-btn:hover { + box-shadow: 0 0 25px #00ffff; + transform: translateY(-2px); +} + +/* Secondary button */ +button.secondary-btn { + background: transparent; + color: #00ffff; + border: 1px solid #00ffff; + padding: 0.8rem 1.5rem; + border-radius: 8px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s ease; +} + +button.secondary-btn:hover { + background: rgba(0, 255, 255, 0.12); + box-shadow: 0 0 20px #00ffff; + transform: translateY(-2px); +} + +/* Responsive adjustments */ +@media (max-width: 600px) { + .landing h2 { + font-size: 1.6rem; + } + + .landing .intro { + font-size: 1rem; + } + + button.primary-btn, + button.secondary-btn { + width: 100%; + } + + .button-group { + flex-direction: column; + gap: 1rem; + } +} diff --git a/src/app/landing/landing.component.ts b/src/app/components/landing/landing.component.ts similarity index 77% rename from src/app/landing/landing.component.ts rename to src/app/components/landing/landing.component.ts index a2026cb..9e3657a 100644 --- a/src/app/landing/landing.component.ts +++ b/src/app/components/landing/landing.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { RouterLink } from '@angular/router'; -import { RouterLinkActive } from "../../../node_modules/@angular/router/router_module.d-Bx9ArA6K"; +import { RouterLinkActive } from "../../../../node_modules/@angular/router/router_module.d-Bx9ArA6K"; @Component({ selector: 'app-landing', diff --git a/src/app/components/leaflet-map/leaflet-map.component.html b/src/app/components/leaflet-map/leaflet-map.component.html new file mode 100644 index 0000000..cf2bf62 --- /dev/null +++ b/src/app/components/leaflet-map/leaflet-map.component.html @@ -0,0 +1,23 @@ + + Select City + + + + + @for (c of filteredCities; track c.name) { + {{ c.name }} + } + + + +
diff --git a/src/app/components/leaflet-map/leaflet-map.component.scss b/src/app/components/leaflet-map/leaflet-map.component.scss new file mode 100644 index 0000000..28c1f77 --- /dev/null +++ b/src/app/components/leaflet-map/leaflet-map.component.scss @@ -0,0 +1,9 @@ +.map-container { + height: 300px; + width: 100%; + margin-top: 1rem; + border: 2px solid #00ffff; + border-radius: 12px; + box-shadow: 0 0 15px rgba(0, 255, 255, 0.4); + box-sizing: border-box; +} \ No newline at end of file diff --git a/src/app/components/leaflet-map/leaflet-map.component.ts b/src/app/components/leaflet-map/leaflet-map.component.ts new file mode 100644 index 0000000..11acda6 --- /dev/null +++ b/src/app/components/leaflet-map/leaflet-map.component.ts @@ -0,0 +1,114 @@ +import { Component, AfterViewInit, ViewChild, ElementRef, forwardRef } from '@angular/core'; +import * as L from 'leaflet'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatInputModule } from '@angular/material/input'; +import { CITY_LIST, City } from '../../shared/cities'; + + + +delete (L.Icon.Default.prototype as any)._getIconUrl; + +L.Icon.Default.mergeOptions({ + iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', + iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', + shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', +}); + + +@Component({ + selector: 'app-leaflet-map', + standalone: true, + imports: [CommonModule, MatFormFieldModule, MatInputModule, MatAutocompleteModule], + templateUrl: './leaflet-map.component.html', + styleUrls: ['./leaflet-map.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => LeafletMapComponent), + multi: true, + }, + ], +}) +export class LeafletMapComponent implements AfterViewInit, ControlValueAccessor { + cities = [...CITY_LIST].sort((a, b) => a.name.localeCompare(b.name)); + filteredCities: City[] = this.cities; + inputValue = ''; + disabled = false; + + map!: L.Map; + marker?: L.Marker; + + @ViewChild('mapEl', { static: true }) mapEl!: ElementRef; + + onChange: (value: string) => void = () => { }; + onTouched: () => void = () => { }; + + ngAfterViewInit(): void { + this.map = L.map(this.mapEl.nativeElement).setView([31.7683, 35.2137], 7); + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap contributors', + }).addTo(this.map); + + + } + + onInput(raw: string | Event): void { + const value = typeof raw === 'string' ? raw : (raw.target as HTMLInputElement).value; + this.inputValue = value; + + const v = value.toLowerCase().trim(); + this.filteredCities = !v + ? this.cities + : this.cities.filter(c => c.name.toLowerCase().includes(v)); + this.onChange(value); + } + + onSelectCity(cityName: string): void { + this.inputValue = cityName; + this.onChange(cityName); + this.onTouched(); + + const city = this.cities.find(c => c.name === cityName); + if (!city || !this.map) return; + + if (this.marker) this.map.removeLayer(this.marker); + this.marker = L.marker([city.lat, city.lng]).addTo(this.map); + this.map.setView([city.lat, city.lng], 12); + } + + onEnter(): void { + const exact = this.cities.find(c => c.name.toLowerCase() === this.inputValue.toLowerCase().trim()); + if (exact) this.onSelectCity(exact.name); + + else this.onTouched(); + console.log(this.marker); + } + + + writeValue(value: string | null): void { + this.inputValue = value ?? ''; + const city = this.cities.find(c => c.name === value); + if (city && this.map) { + if (this.marker) this.map.removeLayer(this.marker); + this.marker = L.marker([city.lat, city.lng]).addTo(this.map); + this.map.setView([city.lat, city.lng], 12); + } + } + + registerOnChange(fn: (value: string) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } +} diff --git a/src/app/components/registration/registration.component.html b/src/app/components/registration/registration.component.html new file mode 100644 index 0000000..630a966 --- /dev/null +++ b/src/app/components/registration/registration.component.html @@ -0,0 +1,114 @@ + + +
+
+ @if(editMode()){ + + }@else { + + } + @if(editMode()){ +

Spaceflight Candidate Registration Update

+ } + @else { +

Spaceflight Candidate Registration

+ } +
+ +
+ + +
+ +
+ + + + Full Name + + @if (form.get('fullName')?.hasError('required')) { + Name is required + } + + + + + Email + + @if (form.get('email')?.hasError('email')) { + Invalid email + } + + + + + Phone Number + + @if (form.get('phoneNumber')?.hasError('required')) { + Phone number is required + } + @if (form.get('phoneNumber')?.hasError('pattern')) { + Invalid phone number + } + + + + + Age + + @if (form.get('age')?.hasError('min') || form.get('age')?.hasError('max')) { + Only applicants of age 18 - 70 allowed + } + + + + + + + + @if (form.get('cityOrRegion')?.invalid && (form.get('cityOrRegion')?.touched || form.get('cityOrRegion')?.dirty)) { + @if (form.get('cityOrRegion')?.hasError('required')) { + City selection is required + } + @if (form.get('cityOrRegion')?.hasError('invalidCity')) { + Please select a valid city from the list + } +} + + + + Hobbies + + @if (form.get('hobbies')?.hasError('maxLength')) { + Maximum length is 300 characters + } + + + + + Why I am the perfect candidate + + @if (form.get('justification')?.hasError('required')) { + Field is required + } + @if (form.get('justification')?.hasError('maxLength')) { + Maximum length is 300 characters + } + + + + +
+ + +
diff --git a/src/app/components/registration/registration.component.scss b/src/app/components/registration/registration.component.scss new file mode 100644 index 0000000..8984bc3 --- /dev/null +++ b/src/app/components/registration/registration.component.scss @@ -0,0 +1,190 @@ +@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap'); + +/* Fullscreen container */ +.container { + width: 100%; + min-height: 100vh; + margin: 0; + padding: 2rem; + background: radial-gradient(circle at top, #0a0f2c 0%, #000000 100%); + color: #e0e0ff; + font-family: 'Orbitron', Arial, sans-serif; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; +} + +/* Shining text animation */ +@keyframes shine { + 0% { background-position: -200% center; } + 100% { background-position: 200% center; } +} + +/* Header shine */ +.container h2 { + text-align: center; + margin-bottom: 2rem; + font-weight: 700; + font-size: 2rem; + background: linear-gradient(90deg, #00ffff, #ffffff, #00ffff); + background-size: 200% auto; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + -webkit-text-fill-color: transparent; + animation: shine 3s linear infinite; + text-shadow: 0 0 12px rgba(0, 255, 255, 0.6); +} + +/* Form styling */ +.registration-form { + display: flex; + flex-direction: column; + gap: 1rem; + max-width: 700px; + width: 100%; +} + +/* Profile image upload */ +.image-upload { + display: flex; + justify-content: center; + margin-bottom: 1rem; +} + +.image-upload img { + max-height: 150px; + border-radius: 50%; + border: 2px solid #00ffff; + box-shadow: 0 0 15px #00ffff; +} + +/* Material form field overrides */ +.mat-form-field { + width: 100%; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; +} + +.mat-form-field-appearance-outline .mat-form-field-outline { + color: #00ffff; +} + +.mat-input-element { + color: #e0e0ff; +} + +.mat-form-field.mat-focused .mat-form-field-outline-thick { + border-color: #00ffff !important; +} + +/* Inputs and textareas */ +input, textarea { + background: transparent; + color: #e0e0ff; + padding: 0.5rem; +} + +textarea { + resize: vertical; +} + +/* Error messages */ +mat-error { + color: #ff4c4c; + font-size: 0.85rem; + text-shadow: 0 0 2px #ff4c4c; + font-family: 'Orbitron', Arial, sans-serif; + margin-bottom: 0.5rem; + display: block; +} + + + +/* Buttons */ +button { + cursor: pointer; + padding: 0.8rem 1.5rem; + font-weight: 700; + border: none; + border-radius: 8px; + transition: all 0.3s ease; + box-shadow: 0 0 12px rgba(0, 255, 255, 0.3); +} + +button[type="submit"] { + background: #00ffff; + color: #000; +} + +button[type="submit"]:hover { + box-shadow: 0 0 25px #00ffff; + transform: translateY(-2px); +} + +button[type="button"] { + background: transparent; + color: #00ffff; + border: 1px solid #00ffff; +} + +button[type="button"]:hover { + background: rgba(0, 255, 255, 0.12); + box-shadow: 0 0 20px #00ffff; + transform: translateY(-2px); +} + + +/* Footer */ +.footer { + text-align: center; + margin-top: 2rem; + font-size: 0.9rem; + color: #8888ff; +} + +.footer a { + color: #00ffff; + text-decoration: none; +} + +.footer a:hover { + text-decoration: underline; +} + + +.app-leaflet-map { + margin-bottom: 1rem; + +} + + +/* Chrome, Safari, Edge, Opera */ +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +input[type=number] { + -moz-appearance: textfield; +} + +/* Responsive adjustments */ +@media (max-width: 600px) { + .container { + padding: 1rem; + } + h2 { + font-size: 1.6rem; + } + .registration-form { + gap: 0.8rem; + } + button { + width: 100%; + } + +} diff --git a/src/app/components/registration/registration.component.ts b/src/app/components/registration/registration.component.ts new file mode 100644 index 0000000..13bdd66 --- /dev/null +++ b/src/app/components/registration/registration.component.ts @@ -0,0 +1,174 @@ +import { AfterViewInit, Component, ElementRef, inject, input, OnInit, QueryList, signal, ViewChildren } from '@angular/core'; +import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { ImageInputComponent } from "../image-input/image-input.component"; +import { CandidateDataService } from '../../services/candidate-data.service'; +import "@maptiler/leaflet-maptilersdk"; +import { LeafletMapComponent } from "../leaflet-map/leaflet-map.component"; +import { CITY_LIST } from '../../shared/cities'; +import { cityValidator } from '../../validators/city.validator'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { SocketIOService } from '../../services/socket-io.service'; + +const CITY_NAMES = CITY_LIST.map(c => c.name).sort(); +const israeliPhoneRegex = /^(?:(?:(\+?972|\(\+?972\)|\+?\(972\))(?:\s|\.|-)?([1-9]\d?))|(0[23489]{1})|(0[57]{1}[0-9]))(?:\s|\.|-)?([^0\D]{1}\d{2}(?:\s|\.|-)?\d{4})$/; + +@Component({ + selector: 'app-registration', + imports: [ + CommonModule, + ReactiveFormsModule, + MatInputModule, + MatButtonModule, + MatCardModule, + MatFormFieldModule, + ImageInputComponent, + LeafletMapComponent, + MatSnackBarModule, + RouterLink + ], + templateUrl: './registration.component.html', + styleUrls: ['./registration.component.scss'], +}) +export class RegistrationComponent implements OnInit { + + + dataService = inject(CandidateDataService); + fb = inject(FormBuilder); + router = inject(Router); + activatedRoute = inject(ActivatedRoute); + snackBar = inject(MatSnackBar) + socketService = inject(SocketIOService); + previewUrl: string | ArrayBuffer | null = null; + + editMode = signal(false); + applicationId = signal(null); + + + form = this.fb.group({ + fullName: ['', [Validators.required, Validators.minLength(3)]], + email: ['', [Validators.required, Validators.email]], + phoneNumber: ['', [Validators.required, Validators.pattern(israeliPhoneRegex)]], + age: [0, [Validators.required, Validators.min(18), Validators.max(70)]], + cityOrRegion: ['', [Validators.required, cityValidator(CITY_NAMES)]], + hobbies: ['', [Validators.maxLength(300)]], + justification: ['', [Validators.required, Validators.maxLength(300)]], + profileImage: this.fb.control(null, Validators.required), + }); + + ngOnInit(): void { + const idParam = this.activatedRoute.snapshot.paramMap.get('id'); + const url = this.activatedRoute.snapshot.url.map(s => s.path); + + if (url.includes('edit') && idParam) { + this.editMode.set(true); + this.applicationId.set(+idParam); + this.loadCandidate(+idParam); + } else { + this.editMode.set(false); + this.applicationId.set(null); + } + } + + loadCandidate(id: number) { + this.dataService.getApplicationDetails(id).subscribe({ + next: (candidate: any) => { + this.form.patchValue({ + fullName: candidate.fullName, + email: candidate.email, + phoneNumber: candidate.phoneNumber, + age: candidate.age, + cityOrRegion: candidate.cityOrRegion, + hobbies: candidate.hobbies, + justification: candidate.justification, + }); + + if (candidate.profileImageUrl) { + this.previewUrl = candidate.profileImageUrl; + this.form.get('profileImage')?.clearValidators(); + this.form.get('profileImage')?.updateValueAndValidity(); + } + }, + error: err => console.error('Failed to load candidate', err), + }); + } + + + scrollToFirstInvalidField() { + for (const key of Object.keys(this.form.controls)) { + const control = this.form.get(key); + if (control && control.invalid) { + const element = document.querySelector( + `[formControlName="${key}"]` + ) as HTMLElement; + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + element.focus(); + } + break; + } + } + } + + onSubmit() { + if (!this.form.valid) { + this.scrollToFirstInvalidField(); + return; + } + + const formData = new FormData(); + + const value = this.form.value; + + if (value.fullName) formData.append('fullName', value.fullName); + if (value.email) formData.append('email', value.email); + if (value.phoneNumber) formData.append('phoneNumber', value.phoneNumber); + if (value.age !== null && value.age !== undefined) formData.append('age', value.age.toString()); + if (value.cityOrRegion) formData.append('cityOrRegion', value.cityOrRegion); + if (value.hobbies) formData.append('hobbies', value.hobbies); + if (value.justification) formData.append('justification', value.justification); + + const imageFile = this.form.get('profileImage')?.value; + + if (imageFile) { + formData.append('profileImage', imageFile); + } + + if (this.editMode() && this.applicationId()) { + this.dataService.updateCandidateForm(this.applicationId()!, formData).subscribe({ + next: (updatedCandidate) => { + this.snackBar.open('✅ Application updated!', 'Close', { + duration: 5000, + horizontalPosition: 'center', + verticalPosition: 'top', + }); + this.socketService.socket.emit('candidateUpdated', updatedCandidate); + this.router.navigate(['/application-list']); + }, + error: err => console.error('Error updating application', err), + }); + } else { + this.dataService.submitCandidateForm(formData).subscribe({ + next: (newCandidate) => { + this.snackBar.open('✅ Application saved!', 'Close', { + duration: 5000, + horizontalPosition: 'center', + verticalPosition: 'top', + }); + this.socketService.socket.emit('candidateRegistered', newCandidate); + this.form.reset(); + }, + error: err => console.error('Error submitting form', err), + }); + } + } + + goBack(): void { + this.router.navigate(['/landing']); + } +} diff --git a/src/app/image-input/image-input.component.html b/src/app/image-input/image-input.component.html deleted file mode 100644 index b0fd17e..0000000 --- a/src/app/image-input/image-input.component.html +++ /dev/null @@ -1,28 +0,0 @@ -
-

Angular material image input

-
- - Photo -
- @if(editForm.get('photo')!.value){ - - - } - - @if(!editForm.get('photo')!.value){ -
- file_upload - - Drag and drop here -
- } - - -
- -
-
-
\ No newline at end of file diff --git a/src/app/image-input/image-input.component.scss b/src/app/image-input/image-input.component.scss deleted file mode 100644 index 6a3e299..0000000 --- a/src/app/image-input/image-input.component.scss +++ /dev/null @@ -1,49 +0,0 @@ -.fileUploadContainer { - padding: 10px; - display: flex; - flex-direction: column; - margin: 0 auto; - width: 150px; - height: 150px; - border: dashed 1px #979797; - text-align: center; - justify-content: center; - - img { - display: block; - margin-left: auto; - margin-right: auto; - max-height: 100%; - max-width: 100%; - } - - .noImageContainet { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - font-size: 11px; - button { - font-size: 11px; - } - } - - .deleteButton{ - position: absolute; - z-index: 10; - top: -25px; - inset-inline-end: -10px; - opacity:50% - } - - .fileInput { - position: absolute; - z-index: 9; - opacity: 0; - height: 100%; - width: 100%; - left: 0px; - top: 0px; - cursor: pointer; - } - } \ No newline at end of file diff --git a/src/app/image-input/image-input.component.ts b/src/app/image-input/image-input.component.ts deleted file mode 100644 index 2d75d56..0000000 --- a/src/app/image-input/image-input.component.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Component, inject } from '@angular/core'; -import { FormsModule, ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatIconModule } from '@angular/material/icon'; -import { CommonModule } from '@angular/common'; - - - - -@Component({ - selector: 'app-image-input', - imports: [ - CommonModule, - MatButtonModule, - MatIconModule, - MatFormFieldModule, - MatInputModule, - FormsModule, - ReactiveFormsModule - ], - templateUrl: './image-input.component.html', - styleUrl: './image-input.component.scss' -}) -export class ImageInputComponent { - - fb = inject(UntypedFormBuilder); - - editForm = this.fb.group({ - photo: [] - }); - - - setFileData(event: Event): void { - const eventTarget: HTMLInputElement | null = event.target as HTMLInputElement | null; - if (eventTarget?.files?.[0]) { - const file: File = eventTarget.files[0]; - const reader = new FileReader(); - reader.addEventListener('load', () => { - this.editForm.get('photo')?.setValue(reader.result as string); - }); - reader.readAsDataURL(file); - } - } -} - diff --git a/src/app/landing/landing.component.html b/src/app/landing/landing.component.html deleted file mode 100644 index 0fbbd11..0000000 --- a/src/app/landing/landing.component.html +++ /dev/null @@ -1,7 +0,0 @@ -

landing works!

- - \ No newline at end of file diff --git a/src/app/landing/landing.component.scss b/src/app/landing/landing.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/page-not-found/page-not-found.component.html b/src/app/page-not-found/page-not-found.component.html new file mode 100644 index 0000000..86176c4 --- /dev/null +++ b/src/app/page-not-found/page-not-found.component.html @@ -0,0 +1,7 @@ +
+

404 - Page Not Found

+

+ The page you're looking for doesn’t exist or has been moved. +

+ +
diff --git a/src/app/page-not-found/page-not-found.component.scss b/src/app/page-not-found/page-not-found.component.scss new file mode 100644 index 0000000..0c2578d --- /dev/null +++ b/src/app/page-not-found/page-not-found.component.scss @@ -0,0 +1,94 @@ +@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap'); + +:host { + display: block; +} + +.container { + width: 100%; + min-height: 100vh; + margin: 0; + padding: 2rem; + background: radial-gradient(circle at top, #0a0f2c 0%, #000000 100%); + color: #e0e0ff; + font-family: 'Orbitron', Arial, sans-serif; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +h2 { + font-size: 2.5rem; + font-weight: 700; + background: linear-gradient(90deg, #00ffff, #ffffff, #00ffff); + background-size: 200% auto; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + -webkit-text-fill-color: transparent; + animation: shine 3s linear infinite; + text-shadow: 0 0 12px rgba(0, 255, 255, 0.6); + margin-bottom: 1.5rem; +} + +button.primary-btn { + background: #00ffff; + color: #000; + padding: 0.8rem 1.5rem; + border-radius: 8px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 0 12px rgba(0, 255, 255, 0.3); +} + +button.primary-btn:hover { + box-shadow: 0 0 25px #00ffff; + transform: translateY(-2px); +} + +.message { + font-size: 1.2rem; + margin-bottom: 2rem; + color: #ccccff; +} + + + +@keyframes shine { + 0% { + background-position: -200% center; + } + 100% { + background-position: 200% center; + } +} + +@media (max-width: 600px) { + h2 { + font-size: 1.8rem; + } + + .message { + font-size: 1rem; + } + + button.primary-btn { + background: #00ffff; + color: #000; + padding: 0.8rem 1.5rem; + border-radius: 8px; + font-weight: 700; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 0 12px rgba(0, 255, 255, 0.3); +} + +button.primary-btn:hover { + box-shadow: 0 0 25px #00ffff; + transform: translateY(-2px); +} +} diff --git a/src/app/page-not-found/page-not-found.component.ts b/src/app/page-not-found/page-not-found.component.ts new file mode 100644 index 0000000..f61183d --- /dev/null +++ b/src/app/page-not-found/page-not-found.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { MatButton, MatButtonModule } from "@angular/material/button"; + +@Component({ + selector: 'app-page-not-found', + templateUrl: './page-not-found.component.html', + styleUrls: ['./page-not-found.component.scss'], + imports: [MatButtonModule] +}) +export class PageNotFoundComponent { + constructor(private router: Router) { } + + goHome(): void { + this.router.navigate(['/']); + } +} diff --git a/src/app/registration/registration.component.html b/src/app/registration/registration.component.html deleted file mode 100644 index 2a71816..0000000 --- a/src/app/registration/registration.component.html +++ /dev/null @@ -1,115 +0,0 @@ -
-

🚀 Spaceflight Candidate Registration

- -
- - - - - Full Name - - @if (form.get('fullName')?.hasError('required')) { - - Name is required - - } - - - - Email - - @if (form.get('email')?.hasError('email')) { - - Invalid email - - } - - - - Phone Number - - @if (form.get('phoneNumber')?.hasError('required')) { - - Phone number is required - - } - @if (form.get('phoneNumber')?.hasError('pattern')){ - - Invalid phone number - - } - - - - Age - - @if (form.get('age')?.hasError('min') || form.get('age')?.hasError('max')) { - - Only aplicants of age 18 - 70 allowed - - } - - - - City / Region - - @if (form.get('cityOrRegion')?.hasError('required')) { - - Field is required - - } - - - - Hobbies - - @if (form.get('hobbies')?.hasError('maxLength')) { - - Maximum length is 300 characters - - } - - - - Why I am the perfect candidate - - @if (form.get('justification')?.hasError('required')) { - - Field is required - - } - @if (form.get('justification')?.hasError('maxLength')) { - - Maximum length is 300 characters - - } - - - - - - -
-
\ No newline at end of file diff --git a/src/app/registration/registration.component.scss b/src/app/registration/registration.component.scss deleted file mode 100644 index 5b2c22a..0000000 --- a/src/app/registration/registration.component.scss +++ /dev/null @@ -1,164 +0,0 @@ -/* Cosmic-themed container styling */ -.registration-container { - max-width: 900px; - margin: 2rem auto; - padding: 2.5rem; - background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%); - color: #e0e0e0; - border-radius: 16px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); - position: relative; - overflow: hidden; -} - -/* Cosmic background overlay */ -.registration-container::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 70%); - opacity: 0.3; - pointer-events: none; -} - -/* Form title with space theme */ -.title { - text-align: center; - font-size: 2.2rem; - font-weight: 700; - color: #00d4ff; - margin-bottom: 2.5rem; - text-transform: uppercase; - letter-spacing: 3px; - text-shadow: 0 0 12px rgba(0, 212, 255, 0.6); - animation: glow 2s ease-in-out infinite alternate; -} - -/* Glowing title animation */ -@keyframes glow { - from { - text-shadow: 0 0 8px rgba(0, 212, 255, 0.4); - } - to { - text-shadow: 0 0 16px rgba(0, 212, 255, 0.8); - } -} - -/* Form styling */ -.form { - display: flex; - flex-direction: column; - gap: 2rem; -} - -/* Full-width form fields */ -.full-width { - width: 100%; -} - -// /* Material form field customization */ -// ::ng-deep .mat-form-field { -// .mat-form-field-label { -// color: #b0bec5; -// font-weight: 500; -// } - -// .mat-form-field-underline { -// background-color: #00d4ff !important; -// } - -// .mat-form-field-ripple { -// background-color: #00d4ff !important; -// } - -// input, textarea { -// color: #e0e0e0; -// background-color: rgba(255, 255, 255, 0.08); -// border-radius: 6px; -// padding: 0.75rem; -// transition: background-color 0.3s ease; -// } - -// input:focus, textarea:focus { -// background-color: rgba(255, 255, 255, 0.12); -// } - -// textarea { -// resize: vertical; -// min-height: 80px; -// } -// } - -/* Error message styling */ -// ::ng-deep .mat-error { -// color: #ff6b6b; -// font-size: 0.9rem; -// } - -/* Upload section */ -.upload-section { - align-items: center; - margin: 2rem 0; - display: flex; - flex-direction: column; - gap: 1.5rem; - - .preview { - margin-top: 1.5rem; - } - - img { - width: 250px; - height: 250px; - border: 4px solid #00d4ff; - box-shadow: 0 0 20px rgba(0, 212, 255, 0.4); - transition: transform 0.3s ease, box-shadow 0.3s ease; - } - - img:hover { - transform: scale(1.1); - box-shadow: 0 0 25px rgba(0, 212, 255, 0.6); - } -} - -/* Responsive design */ -@media (max-width: 768px) { - .registration-container { - margin: 1.5rem; - padding: 2rem; - } - - .title { - font-size: 1.8rem; - margin-bottom: 2rem; - } - - .upload-section img { - max-width: 140px; - } - - // ::ng-deep .mat-raised-button { - // padding: 0.75rem 1.5rem; - // font-size: 0.95rem; - // } -} - -@media (max-width: 480px) { - .registration-container { - margin: 1rem; - padding: 1.5rem; - } - - .title { - font-size: 1.4rem; - margin-bottom: 1.5rem; - } - - .upload-section img { - max-width: 120px; - } - -} \ No newline at end of file diff --git a/src/app/registration/registration.component.ts b/src/app/registration/registration.component.ts deleted file mode 100644 index b47572d..0000000 --- a/src/app/registration/registration.component.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Component, inject, OnInit } from '@angular/core'; -import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; -import { CommonModule } from '@angular/common'; - -import { MatInputModule } from '@angular/material/input'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { ImageInputComponent } from "../image-input/image-input.component"; -import { CandidateDataService } from '../candidate-data.service'; - -const israeliPhoneRegex = /^(?:(?:(\+?972|\(\+?972\)|\+?\(972\))(?:\s|\.|-)?([1-9]\d?))|(0[23489]{1})|(0[57]{1}[0-9]))(?:\s|\.|-)?([^0\D]{1}\d{2}(?:\s|\.|-)?\d{4})$/; -@Component({ - selector: 'app-registration', - imports: [ - CommonModule, - ReactiveFormsModule, - MatInputModule, - MatButtonModule, - MatCardModule, - MatFormFieldModule, - ImageInputComponent - ], - templateUrl: './registration.component.html', - styleUrls: ['./registration.component.scss'], -}) -export class RegistrationComponent implements OnInit { - dataService = inject(CandidateDataService); - fb = inject(FormBuilder); - previewUrl: string | ArrayBuffer | null = null; - - form = this.fb.group({ - fullName: ['', [Validators.required, Validators.minLength(3)]], - email: ['', [Validators.required, Validators.email]], - phoneNumber: ['', [Validators.required, Validators.pattern(israeliPhoneRegex)]], - age: [0, [Validators.required, Validators.min(18), Validators.max(70)]], - cityOrRegion: ['', Validators.required], - hobbies: ['', [Validators.maxLength(300)]], - justification: ['', [Validators.required, Validators.maxLength(300)]], - // profileImage: this.fb.control(null, Validators.required), - }); - - ngOnInit(): void { - const savedData = localStorage.getItem('registration'); - if (!savedData) return; - - const parsed = JSON.parse(savedData); - const dayMilliseconds = 1000 * 60 * 60 * 24; - if (Date.now() - parsed.timestamp < dayMilliseconds * 3) { - this.form.patchValue(parsed.data); - this.previewUrl = parsed.data.profileImage; - } else { - localStorage.removeItem('registration'); - } - } - - // onFileSelected(event: any) { - // const file = event.target.files[0]; - // if (file) { - // const reader = new FileReader(); - // reader.onload = () => { - // if (typeof reader.result === 'string') { - // this.previewUrl = reader.result; - // this.form.patchValue({ profileImage: reader.result }); - // } - // }; - // reader.readAsDataURL(file); - // } - // } - - onSubmit() { - if (!this.form.valid) { - alert("Invalid Form"); - return; - } - console.log(JSON.stringify(this.form.value)); - this.dataService.submitCandidateForm(this.form.value).subscribe(() => { - alert('✅ Application saved! You can re-edit within 3 days.') - }); - } - - onCheckErrors() { - console.log(this.form.controls.age.errors); - } -} diff --git a/src/app/services/candidate-data.service.ts b/src/app/services/candidate-data.service.ts new file mode 100644 index 0000000..3cc61af --- /dev/null +++ b/src/app/services/candidate-data.service.ts @@ -0,0 +1,46 @@ +import { HttpClient } from "@angular/common/http"; +import { inject, Injectable, signal } from "@angular/core"; +import { environment } from "../../environments/environment.development"; +import { delay, tap } from "rxjs"; + +@Injectable({ + providedIn: 'root' +}) +export class CandidateDataService { + httpClient = inject(HttpClient) + isCandidatesListLoading = signal(false) + isApplicationDetailsLoading = signal(false) + cachedApplicationList: any[] = [] + + loadCandidateList() { + this.isCandidatesListLoading.set(true) + return this.httpClient.get(`${environment.hostUrl}/app/candidates`).pipe( + delay(500), + tap((data) => { + this.isCandidatesListLoading.set(false); + this.cachedApplicationList = data; + }) + ); + } + + getApplicationDetails(id: number) { + this.isApplicationDetailsLoading.set(true); + return this.httpClient.get(`${environment.hostUrl}/app/candidate/${id}`).pipe( + delay(500), + tap(() => { this.isApplicationDetailsLoading.set(false) }) + ); + } + + submitCandidateForm(data: FormData) { + return this.httpClient.post(`${environment.hostUrl}/app/register`, data); + } + + updateCandidateForm(id: number, data: FormData) { + return this.httpClient.put( + `${environment.hostUrl}/app/candidate/${id}`, + data + ); + } + + +} \ No newline at end of file diff --git a/src/app/services/socket-io.service.ts b/src/app/services/socket-io.service.ts new file mode 100644 index 0000000..0315594 --- /dev/null +++ b/src/app/services/socket-io.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { io } from "socket.io-client"; + +@Injectable({ + providedIn: 'root' +}) +export class SocketIOService { + + socket = io('ws://localhost:3000'); + + onCandidateRegistered(): Observable { + return new Observable(observer => { + this.socket.on('candidateRegistered', (data) => { + observer.next(data); + }); + }); + } + + onCandidateUpdated(): Observable { + return new Observable(observer => { + this.socket.on('candidateUpdated', (data) => { + observer.next(data); + }); + }); + } + + disconnect() { + this.socket.disconnect(); + } + +} + + + diff --git a/src/app/shared/cities.ts b/src/app/shared/cities.ts new file mode 100644 index 0000000..3c34c56 --- /dev/null +++ b/src/app/shared/cities.ts @@ -0,0 +1,28 @@ +export type City = { + name: string; + lat: number; + lng: number; +}; + +export const CITY_LIST: City[] = [ + { name: 'Jerusalem', lat: 31.7683, lng: 35.2137 }, + { name: 'Tel Aviv', lat: 32.0853, lng: 34.7818 }, + { name: 'Haifa', lat: 32.7940, lng: 34.9896 }, + { name: 'Rishon LeZion', lat: 31.9730, lng: 34.7925 }, + { name: 'Petah Tikva', lat: 32.0840, lng: 34.8878 }, + { name: 'Ashdod', lat: 31.8014, lng: 34.6435 }, + { name: 'Netanya', lat: 32.3215, lng: 34.8532 }, + { name: 'Beer Sheva', lat: 31.2520, lng: 34.7915 }, + { name: 'Holon', lat: 32.0158, lng: 34.7874 }, + { name: 'Bnei Brak', lat: 32.0836, lng: 34.8337 }, + { name: 'Bat Yam', lat: 32.0231, lng: 34.7500 }, + { name: 'Ashkelon', lat: 31.6693, lng: 34.5715 }, + { name: 'Herzliya', lat: 32.1663, lng: 34.8436 }, + { name: 'Kfar Saba', lat: 32.1782, lng: 34.9076 }, + { name: 'Ra’anana', lat: 32.1848, lng: 34.8713 }, + { name: 'Ramat Gan', lat: 32.0684, lng: 34.8248 }, + { name: 'Lod', lat: 31.9516, lng: 34.8883 }, + { name: 'Nazareth', lat: 32.6996, lng: 35.3035 }, + { name: 'Eilat', lat: 29.5577, lng: 34.9519 }, + { name: 'Tiberias', lat: 32.7959, lng: 35.5309 }, +]; diff --git a/src/app/validators/city.validator.ts b/src/app/validators/city.validator.ts new file mode 100644 index 0000000..0e4f598 --- /dev/null +++ b/src/app/validators/city.validator.ts @@ -0,0 +1,11 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +export function cityValidator(cities: string[]): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value?.trim(); + if (!value) return null; + return cities.some(c => c.toLowerCase() === value.toLowerCase()) + ? null + : { invalidCity: true }; + }; +} \ No newline at end of file diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 3a769d3..9b87101 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,3 +1,5 @@ export const environment = { - hostUrl: 'http://localhost:3000' + hostUrl: 'http://localhost:3000', + mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce' + }; diff --git a/src/environments/environment.remote-api.ts b/src/environments/environment.remote-api.ts index 647037f..ddbb408 100644 --- a/src/environments/environment.remote-api.ts +++ b/src/environments/environment.remote-api.ts @@ -1,3 +1,5 @@ export const environment = { - hostUrl: 'https://iisa.novikov.click' + hostUrl: 'https://iisa.novikov.click', + mapTilerApiKey: '9LJZ0OppHyT3LzvQW3ce' + }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 839370b..86b78f9 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,3 +1,4 @@ export const environment = { - hostUrl: '' + hostUrl: '', + mapTilerApiKey:'' }; diff --git a/src/index.html b/src/index.html index 6cd995a..45698a2 100644 --- a/src/index.html +++ b/src/index.html @@ -7,6 +7,7 @@ +