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 @@
+
+
+
+ @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 @@
+
+
+
+ @if (isApplicationDetailsLoading) {
+
+
+
Loading application details...
+
+ } @else if (hasApplicationData) {
+
+
+
+
+
+
+
+ {{ currentIndex() + 1 }} of {{ applicationList().length }}
+
+
+
+
+
+
+
+
+
+
+ @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 @@
+
+
+
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
-
-
\ 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
-
-
-
\ 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 @@
+