├── .browserslistrc ├── .editorconfig ├── .env.ref ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .gitpod.yml ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── inspectionProfiles │ ├── Project_All.xml │ ├── Project_Default.xml │ └── profiles_settings.xml ├── jsLibraryMappings.xml ├── misc.xml ├── modules.xml ├── project.iml └── vcs.xml ├── .madgerc ├── .prettierignore ├── .storybook ├── main.ts ├── preview.ts └── webpack.ts ├── .stylelintignore ├── .vscode └── settings.json ├── .yarnclean ├── Dockerfile ├── README.md ├── azure-pipelines.yml ├── babel.config.js ├── frontend.sh ├── jest.config.js ├── netlify.toml ├── nginx.conf ├── ngx.conf ├── package.json ├── prettier.config.js ├── src ├── @ │ ├── country │ │ ├── effects │ │ │ ├── add-countries-fx.ts │ │ │ ├── add-countries-layer.ts │ │ │ ├── add-global-schools-layer.ts │ │ │ ├── get-schools-colors.ts │ │ │ ├── index.ts │ │ │ ├── leave-country-route-fx.ts │ │ │ ├── remove-country-fx.ts │ │ │ ├── remove-schools-fx.ts │ │ │ ├── update-country-fx.ts │ │ │ ├── update-global-schools-fx.ts │ │ │ ├── update-schools-colors-fx.ts │ │ │ ├── update-schools-fx.ts │ │ │ └── zoom-to-country-fx.ts │ │ ├── init.ts │ │ ├── lib │ │ │ ├── get-countries-geo-json.ts │ │ │ ├── get-global-schools-geo-json.ts │ │ │ ├── get-polygon-bounding-box.ts │ │ │ ├── get-schools-geojson.ts │ │ │ └── index.ts │ │ ├── model.ts │ │ └── types.ts │ ├── dashboard │ │ ├── constats.ts │ │ ├── init.ts │ │ ├── model.ts │ │ ├── types.ts │ │ └── ui │ │ │ ├── country-list.tsx │ │ │ ├── county-popup.tsx │ │ │ ├── dashboard-description.tsx │ │ │ ├── dashboard.tsx │ │ │ ├── get-country-info.ts │ │ │ ├── index.ts │ │ │ ├── search-sort-box.tsx │ │ │ └── search-sort-results.tsx │ ├── history-modal │ │ ├── init.ts │ │ ├── lib │ │ │ ├── get-history-graph-data.ts │ │ │ └── index.ts │ │ ├── model.ts │ │ ├── types.ts │ │ └── ui │ │ │ ├── history-modal.tsx │ │ │ └── index.ts │ ├── map │ │ ├── constants.ts │ │ ├── effects │ │ │ ├── add-loader-fx.ts │ │ │ ├── change-style-fx.ts │ │ │ ├── index.ts │ │ │ ├── init-map-fx.ts │ │ │ └── remove-loader-fx.ts │ │ ├── init.ts │ │ ├── model.ts │ │ ├── tests │ │ │ ├── model.test.ts │ │ │ └── ui.test.tsx │ │ ├── types.ts │ │ └── ui │ │ │ ├── footer.stories.tsx │ │ │ ├── footer.tsx │ │ │ ├── header.stories.tsx │ │ │ ├── header.tsx │ │ │ ├── index.ts │ │ │ ├── map-page.stories.tsx │ │ │ ├── map-page.tsx │ │ │ ├── map.stories.tsx │ │ │ ├── map.tsx │ │ │ └── underlay.tsx │ ├── popup │ │ ├── effects │ │ │ ├── add-school-popup-fx.ts │ │ │ ├── create-school-popup-fx.ts │ │ │ └── index.ts │ │ ├── init.ts │ │ ├── lib │ │ │ ├── get-school-info.ts │ │ │ └── index.ts │ │ ├── model.ts │ │ ├── types.ts │ │ └── ui │ │ │ ├── index.ts │ │ │ └── popup.tsx │ ├── project │ │ ├── init.ts │ │ ├── model.ts │ │ ├── types.ts │ │ └── ui │ │ │ ├── about.tsx │ │ │ ├── country-progress.tsx │ │ │ ├── footer.tsx │ │ │ ├── header.tsx │ │ │ ├── index.ts │ │ │ ├── join-us.tsx │ │ │ ├── media.tsx │ │ │ ├── privacy.tsx │ │ │ ├── project-page.stories.tsx │ │ │ ├── project-page.tsx │ │ │ └── slider.tsx │ ├── scroll │ │ ├── index.ts │ │ ├── instant-scroll-fx.ts │ │ ├── scroll-style.ts │ │ ├── scroll-to-hash-fx.ts │ │ ├── scroll.ts │ │ └── types.ts │ ├── sidebar │ │ ├── constants.ts │ │ ├── init.ts │ │ ├── model.ts │ │ ├── sort-countries.ts │ │ ├── tests │ │ │ └── ui.test.tsx │ │ ├── types.ts │ │ └── ui │ │ │ ├── controls-sort.tsx │ │ │ ├── controls.tsx │ │ │ ├── country-info.tsx │ │ │ ├── country-list-item.tsx │ │ │ ├── country-list.tsx │ │ │ ├── get-country-info.ts │ │ │ ├── index.ts │ │ │ ├── pie-chart.tsx │ │ │ ├── search-results.tsx │ │ │ ├── search.stories.tsx │ │ │ ├── search.tsx │ │ │ ├── sidebar.stories.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── sort.tsx │ │ │ ├── tabs.tsx │ │ │ ├── types.ts │ │ │ └── world-view.tsx │ └── week-graph │ │ ├── constants.ts │ │ ├── lib │ │ ├── get-week-graph-data.ts │ │ └── index.ts │ │ ├── types.ts │ │ └── ui │ │ ├── index.ts │ │ └── week-graph.tsx ├── __mocks__ │ ├── jest.stub.js │ ├── match-media.js │ └── styleMock.js ├── api │ ├── project-connect.ts │ └── types.ts ├── app.tsx ├── assets │ ├── fonts │ │ ├── Cabin │ │ │ ├── Cabin-Bold.ttf │ │ │ ├── Cabin-Medium.ttf │ │ │ ├── Cabin-Regular.ttf │ │ │ ├── METADATA.pb │ │ │ ├── OFL.txt │ │ │ ├── cabin-400.css │ │ │ ├── cabin-400.latin.woff2 │ │ │ ├── cabin-500.css │ │ │ ├── cabin-500.latin.woff2 │ │ │ ├── cabin-700.css │ │ │ └── cabin-700.latin.woff2 │ │ ├── Roboto │ │ │ ├── METADATA.pb │ │ │ ├── OFL.txt │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-Medium.ttf │ │ │ ├── Roboto-Regular.ttf │ │ │ ├── roboto-400.css │ │ │ ├── roboto-400.cyrillic.woff2 │ │ │ ├── roboto-400.latin.woff2 │ │ │ ├── roboto-500.css │ │ │ ├── roboto-500.cyrillic.woff2 │ │ │ ├── roboto-500.latin.woff2 │ │ │ ├── roboto-700.css │ │ │ ├── roboto-700.cyrillic.woff2 │ │ │ └── roboto-700.latin.woff2 │ │ └── styles.ts │ ├── images │ │ ├── Kenya1.jpg │ │ ├── Kenya2.jpg │ │ ├── Kyrgyzstan1.jpg │ │ ├── Kyrgyzstan2.jpg │ │ ├── SierraLeone1.jpg │ │ ├── SierraLeone2.jpg │ │ ├── about-media.jpg │ │ ├── case-studies.jpg │ │ ├── chevron.svg │ │ ├── child-protection.svg │ │ ├── connectivity-map.jpg │ │ ├── countries-dashboard.jpg │ │ ├── data-sharing-1.jpg │ │ ├── data-sharing-2.jpg │ │ ├── giga-logo-footer.svg │ │ ├── icon-accountability.svg │ │ ├── icon-back-arrow.svg │ │ ├── icon-data-collection.svg │ │ ├── icon-data-equity.svg │ │ ├── icon-download.svg │ │ ├── icon-facebook-logo.svg │ │ ├── icon-github-logo.svg │ │ ├── icon-history.svg │ │ ├── icon-information-for-planning.svg │ │ ├── icon-investment.svg │ │ ├── icon-last-data.svg │ │ ├── icon-left-arrow.svg │ │ ├── icon-location-cross.svg │ │ ├── icon-location.svg │ │ ├── icon-machine-learning.svg │ │ ├── icon-market-data.svg │ │ ├── icon-measurement.svg │ │ ├── icon-more-info-white-copy.png │ │ ├── icon-more-info-white-copy.svg │ │ ├── icon-partnerships.svg │ │ ├── icon-right-arrow.svg │ │ ├── icon-search.svg │ │ ├── icon-speed-high.svg │ │ ├── icon-speed-low.svg │ │ ├── icon-speed-medium.svg │ │ ├── icon-twitter-logo.svg │ │ ├── itu-logo-footer.svg │ │ ├── join-us.jpg │ │ ├── list.svg │ │ ├── logo-actual.svg │ │ ├── logo-arm.svg │ │ ├── logo-development-seed.svg │ │ ├── logo-dubai-cares.svg │ │ ├── logo-ericsson.svg │ │ ├── logo-gsma.svg │ │ ├── logo-mapbox.svg │ │ ├── logo-maxar.svg │ │ ├── logo-mlab.svg │ │ ├── logo-musk-foundation.svg │ │ ├── logo-nic-br.svg │ │ ├── logo-softbank.svg │ │ ├── logo-tryo-labs.svg │ │ ├── map-with-hand.svg │ │ ├── media-post-1.jpg │ │ ├── media-post-2.jpg │ │ ├── media-post-3.jpg │ │ ├── media-post-4.jpg │ │ ├── media-post-5.gif │ │ ├── media-post-6.jpg │ │ ├── media-post-7.png │ │ ├── media-post-8.png │ │ ├── preview-placeholder.jpg │ │ ├── public-goods.svg │ │ ├── school-location.svg │ │ ├── search.svg │ │ ├── slider-colombia.jpg │ │ ├── tile.svg │ │ ├── unicef-logo-footer.svg │ │ ├── unicef-logo-map-footer.svg │ │ └── why-mapping.jpg │ └── styles │ │ ├── abstracts │ │ ├── _mixins.scss │ │ └── _variables.scss │ │ ├── app.scss │ │ ├── base │ │ ├── _typography.scss │ │ └── _utilities.scss │ │ ├── components │ │ ├── _about-intro.scss │ │ ├── _average-speed.scss │ │ ├── _breadcrumbs.scss │ │ ├── _button.scss │ │ ├── _case-studies.scss │ │ ├── _chevron.scss │ │ ├── _controls-bar.scss │ │ ├── _countries-list.scss │ │ ├── _country-progress.scss │ │ ├── _country.scss │ │ ├── _definition-list.scss │ │ ├── _feedback.scss │ │ ├── _footer-menu.scss │ │ ├── _footer-socials.scss │ │ ├── _form.scss │ │ ├── _history-modal.scss │ │ ├── _history-speed-graph.scss │ │ ├── _info-list.scss │ │ ├── _input.scss │ │ ├── _join-us-select.scss │ │ ├── _link.scss │ │ ├── _list.scss │ │ ├── _map-hint.scss │ │ ├── _map-legend.scss │ │ ├── _map-loader.scss │ │ ├── _map-resizer.scss │ │ ├── _map-switcher.scss │ │ ├── _mapping-list.scss │ │ ├── _mapping.scss │ │ ├── _not-found.scss │ │ ├── _page-heading.scss │ │ ├── _partners-list.scss │ │ ├── _partners.scss │ │ ├── _partnership.scss │ │ ├── _period-picker.scss │ │ ├── _pie-chart.scss │ │ ├── _post.scss │ │ ├── _progress-dashboard.scss │ │ ├── _radio-group.scss │ │ ├── _radio.scss │ │ ├── _school-popup.scss │ │ ├── _schools-connectivity.scss │ │ ├── _search-bar.scss │ │ ├── _section.scss │ │ ├── _select.scss │ │ ├── _sidebar.scss │ │ ├── _slider-animations.scss │ │ ├── _slider-navigation.scss │ │ ├── _slider-pagination.scss │ │ ├── _slider.scss │ │ ├── _status-list.scss │ │ ├── _tabs.scss │ │ ├── _textarea.scss │ │ ├── _tooltip.scss │ │ ├── _view-changer.scss │ │ ├── _view-connectivity.scss │ │ ├── _view-on-map.scss │ │ └── _week-graph.scss │ │ ├── init.scss │ │ ├── layout │ │ ├── _content.scss │ │ ├── _footer.scss │ │ ├── _header.scss │ │ └── _navigation.scss │ │ ├── main.scss │ │ ├── pages │ │ ├── _info.scss │ │ └── _map.scss │ │ └── vendor │ │ ├── bootstrap │ │ ├── _breakpoint.scss │ │ └── _grid.scss │ │ └── mapbox │ │ └── _mapbox-popup.scss ├── core │ ├── app-frame.tsx │ ├── formatters.ts │ ├── global-style.ts │ ├── index.ts │ ├── init.ts │ ├── media-query.ts │ ├── root.tsx │ └── routes.ts ├── env.ts ├── favicon.png ├── hot-app.ts ├── index.html ├── index.ts ├── lib │ ├── click-outside │ │ └── use-click-outside.tsx │ ├── date-fns-kit │ │ ├── defaults.ts │ │ ├── format-date-interval.ts │ │ ├── get-interval.ts │ │ ├── index.ts │ │ ├── is-current-interval.ts │ │ └── types.ts │ ├── effector-kit │ │ ├── helpers.ts │ │ ├── index.ts │ │ └── types.ts │ ├── event-reducers │ │ ├── index.ts │ │ ├── input-value.ts │ │ └── select-value.ts │ ├── human-format │ │ ├── human-format.ts │ │ ├── index.ts │ │ └── test.ts │ ├── media-query │ │ ├── create-media-matcher.ts │ │ └── index.ts │ ├── request-fx │ │ ├── create-controller.ts │ │ ├── create-request-fx.ts │ │ ├── domain.ts │ │ ├── examples.ts │ │ ├── index.ts │ │ ├── readme.md │ │ ├── test.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── request │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── router │ │ ├── index.ts │ │ ├── react.tsx │ │ ├── route.ts │ │ ├── router.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── storybook-kit │ │ ├── create-decorator.ts │ │ ├── create-variant.ts │ │ └── index.ts │ ├── test-kit │ │ ├── argument-history.ts │ │ └── index.ts │ └── types │ │ └── index.ts ├── modules.d.ts ├── stories │ └── welcome │ │ ├── assets │ │ ├── code-brackets.svg │ │ ├── comments.svg │ │ ├── direction.svg │ │ └── repo.svg │ │ └── welcome.stories.mdx └── ui │ ├── button.stories.mdx │ ├── button.tsx │ ├── dropdown.tsx │ ├── index.ts │ ├── join-us-dropdown.tsx │ ├── layout.tsx │ ├── logo.tsx │ ├── main.tsx │ ├── progress-bar.tsx │ └── radio-buttons.tsx ├── sshd_config ├── stylelint.config.js ├── test.env ├── tsconfig.json ├── webpack ├── config.common.ts ├── config.dev.ts ├── config.info.ts ├── config.prod.ts ├── env.ts ├── paths.ts ├── postcss.ts ├── rules.ts └── types.ts └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | [production] 2 | 3 | Chrome >= 83 4 | ChromeAndroid >= 83 5 | Firefox >= 78 6 | Safari >= 13.1 7 | iOS >= 12.4 8 | Edge >= 83 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | ij_continuation_indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.env.ref: -------------------------------------------------------------------------------- 1 | # A reference template for the ".env" file 2 | # README: https://www.npmjs.com/package/dotenv 3 | # All vars referenced as process.env.[VAR_NAME] in the code should go here. 4 | # Environment variables (required, ending with "="): 5 | 6 | API_MAPBOX_ACCESS_TOKEN 7 | API_BASE_URL 8 | RECAPTCHA_KEY 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # TS 2 | .typescript 3 | 4 | # Storybook 5 | !.storybook 6 | 7 | # Dependencies 8 | node_modules 9 | 10 | # Ignore npm lock (use yarn.lock) 11 | package-lock.json 12 | 13 | # Build files 14 | build 15 | dist 16 | storybook-static 17 | 18 | # Misc 19 | .coverage 20 | .temp 21 | .npm 22 | .cache 23 | .*-cache 24 | 25 | # DotEnv 26 | .env 27 | 28 | # VSCode 29 | .vscode 30 | 31 | # WebStorm 32 | .idea 33 | 34 | # System 35 | *.DS_Store 36 | .AppleDouble 37 | .LSOverride 38 | Thumbs.db 39 | ehthumbs.db 40 | *.lnk 41 | 42 | # Logs 43 | *.log 44 | npm-debug.log* 45 | yarn-debug.log* 46 | yarn-error.log* 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # TS 2 | .typescript 3 | 4 | # Dependencies 5 | node_modules 6 | 7 | # Ignore npm lock (use yarn.lock) 8 | package-lock.json 9 | 10 | # Build files 11 | build 12 | dist 13 | storybook-static 14 | 15 | # Misc 16 | .coverage 17 | .temp 18 | .npm 19 | .cache 20 | .*-cache 21 | 22 | # DotEnv 23 | .env 24 | 25 | # VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/launch.json 29 | !.vscode/extensions.json 30 | 31 | # WebStorm 32 | .idea/* 33 | 34 | # Include workspace settings 35 | !.idea/codeStyles 36 | !.idea/inspectionProfiles 37 | !.idea/dictionaries/doasync.xml 38 | !.idea/compiler.xml 39 | !.idea/jsLibraryMappings.xml 40 | !.idea/misc.xml 41 | !.idea/modules.xml 42 | !.idea/vcs.xml 43 | 44 | # Project 45 | !.idea/project.iml 46 | 47 | # System 48 | .DS_Store 49 | *.DS_Store 50 | .AppleDouble 51 | .LSOverride 52 | Thumbs.db 53 | ehthumbs.db 54 | *.lnk 55 | 56 | # Logs 57 | *.log 58 | npm-debug.log* 59 | yarn-debug.log* 60 | yarn-error.log* 61 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: yarn 3 | - command: yarn start 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/project.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.madgerc: -------------------------------------------------------------------------------- 1 | { 2 | "fileExtensions": [ 3 | "ts", 4 | "tsx", 5 | "js", 6 | "jsx" 7 | ], 8 | "excludeRegExp": [ 9 | ".(jpg|png|gif|woff2|svg|json|css|scss)$" 10 | ], 11 | "tsConfig": "tsconfig.json", 12 | "layout": "dot" 13 | } 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Prettier 2 | .madgerc 3 | test.env 4 | yarn.lock 5 | .yarnclean 6 | .env.ref 7 | .editorconfig 8 | .gitattributes 9 | .browserslistrc 10 | .nvmrc 11 | *.*ignore 12 | *.jpg 13 | *.png 14 | *.gif 15 | *.svg 16 | *.woff2 17 | *.ttf 18 | *.txt 19 | *.pb 20 | *.toml 21 | 22 | # TS 23 | .typescript 24 | 25 | # Dependencies 26 | node_modules 27 | 28 | # Ignore npm lock (use yarn.lock) 29 | package-lock.json 30 | 31 | # Build files 32 | build 33 | dist 34 | storybook-static 35 | 36 | # Misc 37 | .coverage 38 | .temp 39 | .npm 40 | .cache 41 | .*-cache 42 | 43 | # DotEnv 44 | .env 45 | 46 | # VSCode 47 | .vscode 48 | 49 | # WebStorm 50 | .idea 51 | 52 | # System 53 | *.DS_Store 54 | .AppleDouble 55 | .LSOverride 56 | Thumbs.db 57 | ehthumbs.db 58 | *.lnk 59 | 60 | # Logs 61 | *.log 62 | npm-debug.log* 63 | yarn-debug.log* 64 | yarn-error.log* 65 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { StorybookConfig } from '@storybook/core/types'; 2 | 3 | import { configureWebpack } from './webpack'; 4 | 5 | export const storybookConfig: StorybookConfig = { 6 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 7 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 8 | webpackFinal: configureWebpack, 9 | }; 10 | 11 | export default storybookConfig; 12 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: '^on[A-Z].*' }, 3 | }; 4 | -------------------------------------------------------------------------------- /.storybook/webpack.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-imports */ 2 | 3 | import { Configuration } from 'webpack'; 4 | import { merge } from 'webpack-merge'; 5 | 6 | import { resolvePlugins } from '../webpack/config.common'; 7 | import { createRules } from '../webpack/rules'; 8 | 9 | const customConfig: Configuration = { 10 | resolve: { 11 | plugins: resolvePlugins, 12 | }, 13 | module: { 14 | rules: createRules({ inlineCssOnly: true, excludeJs: true }), 15 | }, 16 | performance: { 17 | hints: false, 18 | }, 19 | }; 20 | 21 | export const configureWebpack = (config: Configuration): Configuration => { 22 | // Remove style-loader, file-loader, url-loader 23 | if (config.module) { 24 | // eslint-disable-next-line no-param-reassign 25 | config.module.rules = config.module?.rules?.slice(0, -3) ?? []; 26 | } 27 | 28 | // Merge with custom config 29 | return merge(config, customConfig); 30 | }; 31 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # Stylelint 2 | *.json 3 | *.jpg 4 | *.png 5 | *.gif 6 | *.svg 7 | *.woff2 8 | *.ttf 9 | *.txt 10 | *.pb 11 | 12 | # Dependencies 13 | node_modules 14 | 15 | # Build files 16 | build 17 | dist 18 | storybook-static 19 | 20 | # Misc 21 | .coverage 22 | .temp 23 | .npm 24 | .cache 25 | .*-cache 26 | 27 | # Logs 28 | *.log 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "telemetry.enableTelemetry": false, 3 | "workbench.fontAliasing": "default", 4 | "editor.tabSize": 2, 5 | "editor.formatOnSave": true, 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": true, 9 | "source.fixAll.stylelint": true 10 | }, 11 | "eslint.alwaysShowStatus": true, 12 | "eslint.format.enable": true, 13 | "cSpell.words": [ 14 | "woff", 15 | "babelrc", 16 | "browserlist", 17 | "entrypoints", 18 | "pathinfo", 19 | "prettierrc" 20 | ], 21 | "editor.wordSeparators": "`~!@#%^&*()-=+[{]}\\|;:'\",.<>/?" 22 | } 23 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | @types/react-native 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1 2 | FROM node:15 AS s1 3 | WORKDIR /proco 4 | COPY package.json ./ 5 | RUN npm install 6 | COPY . . 7 | 8 | ARG RECAPTCHA_KEY 9 | ENV RECAPTCHA_KEY=$RECAPTCHA_KEY 10 | ARG API_MAPBOX_ACCESS_TOKEN 11 | ENV API_MAPBOX_ACCESS_TOKEN=$API_MAPBOX_ACCESS_TOKEN 12 | ARG API_BASE_URL 13 | ENV API_BASE_URL=$API_BASE_URL 14 | 15 | RUN yarn build 16 | 17 | # Stage 2 18 | FROM nginx:alpine AS s2 19 | 20 | # ssh 21 | ENV SSH_PASSWD "root:Docker!" 22 | RUN apt-get update \ 23 | && apt-get install -y --no-install-recommends dialog \ 24 | && apt-get update \ 25 | && apt-get install -y --no-install-recommends openssh-server \ 26 | && echo "$SSH_PASSWD" | chpasswd 27 | 28 | COPY sshd_config /etc/ssh/ 29 | 30 | # add built frontend 31 | RUN rm -rf /usr/share/nginx/html/* 32 | COPY --from=s1 /proco/build /usr/share/nginx/html 33 | COPY nginx.conf /etc/nginx/conf.d/default.conf 34 | COPY ngx.conf /etc/nginx/nginx.conf 35 | 36 | EXPOSE 80 2222 37 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master_frontend 3 | 4 | pool: 5 | vmImage: 'ubuntu-18.04' 6 | variables: 7 | dockerRegistryServiceConnection: 'UNICEF_DATA_CONNECT_WEB_ACR' 8 | dockerfilePath: './Dockerfile' 9 | app: 'project-connect-frontend' 10 | imageRepository: 'dev/$(app)' 11 | tag: '$(Build.BuildId)' 12 | 13 | steps: 14 | # Docker build and push 15 | - task: Docker@2 16 | displayName: Build image 17 | inputs: 18 | command: build 19 | repository: $(imageRepository) 20 | containerRegistry: $(dockerRegistryServiceConnection) 21 | dockerfile: $(dockerfilePath) 22 | arguments: --build-arg=API_BASE_URL=$(apiBaseUrl) --build-arg=API_MAPBOX_ACCESS_TOKEN=$(mapboxToken) 23 | - task: Docker@2 24 | displayName: Push image to container registry 25 | inputs: 26 | command: push 27 | repository: $(imageRepository) 28 | containerRegistry: $(dockerRegistryServiceConnection) 29 | tags: | 30 | $(tag) 31 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const isTest = process.env.NODE_ENV === 'test'; 2 | const isDevelopment = 3 | process.env.WEBPACK_DEV_SERVER === 'true' || 4 | process.env.NODE_ENV === 'development'; 5 | 6 | const presetReact = { 7 | development: isDevelopment, 8 | useBuiltIns: true, 9 | }; 10 | 11 | /** @type import('@babel/preset-env').Options */ 12 | const presetEnv = { 13 | loose: true, 14 | useBuiltIns: 'usage', 15 | corejs: 3, 16 | modules: isTest ? 'commonjs' : false, 17 | shippedProposals: true, 18 | bugfixes: true, // Remove later in babel 8 19 | }; 20 | 21 | const presetTypescript = { 22 | isTSX: true, 23 | allExtensions: true, 24 | }; 25 | 26 | const pluginStyledComponents = { 27 | displayName: isDevelopment, 28 | pure: true, 29 | }; 30 | 31 | const pluginEffector = { 32 | addLoc: true, 33 | importName: ['effector', 'effector-logger'], 34 | }; 35 | 36 | module.exports = { 37 | presets: [ 38 | ['@babel/preset-react', presetReact], 39 | ['@babel/preset-env', presetEnv], 40 | ['@babel/preset-typescript', presetTypescript], 41 | ], 42 | plugins: [ 43 | ['babel-plugin-styled-components', pluginStyledComponents], 44 | ['react-hot-loader/babel'], 45 | ['effector/babel-plugin', (isDevelopment || isTest) && pluginEffector], 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /frontend.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | # startup script to be used for debugging 5 | 6 | # export environment variables to make them available in ssh session 7 | for var in $(compgen -e); do 8 | echo "export $var=${!var}" >> /etc/profile 9 | done 10 | 11 | echo "Starting SSH ..." 12 | service ssh start 13 | 14 | nginx -g daemon off; 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | // Set env vars from file 4 | require('dotenv').config({ path: 'test.env' }); 5 | 6 | // Override system NODE_ENV 7 | process.env.NODE_ENV = 'test'; 8 | 9 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 10 | const { compilerOptions } = require('./tsconfig.json'); 11 | 12 | // Transforms tsconfig.json paths to jest's moduleNameMapper 13 | const tsModuleNameMap = pathsToModuleNameMapper(compilerOptions.paths, { 14 | prefix: `${__dirname}/`, 15 | }); 16 | 17 | // noinspection JSValidateJSDoc 18 | /** @type import('@jest/types').Config.InitialOptions */ 19 | // noinspection JSValidateJSDoc 20 | /** @type import('@jest/types').Config.InitialOptions */ 21 | const config = { 22 | cacheDirectory: 'node_modules/.cache/jest', 23 | coverageDirectory: '.coverage', 24 | moduleNameMapper: { 25 | ...tsModuleNameMap, 26 | '\\.(css|scss)$': '/src/__mocks__/styleMock.js', 27 | }, 28 | transform: { 29 | '^.+\\.tsx?$': 'babel-jest', 30 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 31 | 'jest-transform-stub', 32 | '^.+\\.svg$': 'jest-svg-transformer', 33 | }, 34 | setupFiles: ['/src/__mocks__/jest.stub.js'], 35 | }; 36 | 37 | module.exports = config; 38 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # The following redirect is intended for use with most SPAs that handle 2 | # routing internally. 3 | [[redirects]] 4 | from = "/*" 5 | to = "/index.html" 6 | status = 200 7 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | root /usr/share/nginx/html; 5 | index index.html; 6 | 7 | client_max_body_size 20M; 8 | 9 | location / { 10 | try_files $uri /index.html; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ngx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | #tcp_nopush on; 25 | 26 | keepalive_timeout 65; 27 | 28 | gzip on; 29 | gzip_min_length 1000; 30 | gzip_types text/html application/x-javascript text/css application/javascript text/javascript text/plain text/xml application/json application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype application/x-font-ttf application/xml font/eot font/opentype font/otf image/svg+xml image/vnd.microsoft.icon; 31 | gzip_disable "MSIE [1-6]\."; 32 | 33 | 34 | include /etc/nginx/conf.d/*.conf; 35 | } -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type import('prettier').Options */ 2 | const config = { 3 | singleQuote: true, 4 | proseWrap: 'always', 5 | }; 6 | 7 | module.exports = config; 8 | -------------------------------------------------------------------------------- /src/@/country/effects/add-countries-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | // eslint-disable-next-line no-restricted-imports 4 | import { addCountriesLayer } from '@/country/effects/add-countries-layer'; 5 | import { AddCountries } from '@/country/types'; 6 | 7 | export const addCountriesFx = createEffect( 8 | ({ map, paintData, countriesGeoJson, countryCode }: AddCountries) => { 9 | if (!map || !countriesGeoJson) return; 10 | 11 | map.addSource('countries', { 12 | type: 'geojson', 13 | data: countriesGeoJson, 14 | }); 15 | 16 | if (!countryCode) { 17 | addCountriesLayer({ map, paintData }); 18 | } 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /src/@/country/effects/add-global-schools-layer.ts: -------------------------------------------------------------------------------- 1 | import { getGlobalSchoolsColor } from '@/map/constants'; 2 | import { Map, StylePaintData } from '@/map/types'; 3 | 4 | export const addGlobalSchoolsLayer = ({ 5 | map, 6 | paintData, 7 | }: { 8 | map: Map; 9 | paintData: StylePaintData; 10 | }): void => { 11 | map.addLayer({ 12 | id: 'schoolsGlobal', 13 | type: 'circle', 14 | source: 'schoolsGlobal', 15 | paint: { 16 | 'circle-radius': { 17 | stops: [ 18 | [1, 0.5], 19 | [2, 0.5], 20 | [3, 0.5], 21 | [4, 1], 22 | [8, 2], 23 | ], 24 | }, 25 | 'circle-color': getGlobalSchoolsColor(paintData), 26 | }, 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/@/country/effects/get-schools-colors.ts: -------------------------------------------------------------------------------- 1 | import { Expression, StyleFunction } from 'mapbox-gl'; 2 | 3 | import { MapType, StylePaintData } from '@/map/types'; 4 | 5 | type GetSchoolsColors = { 6 | mapType: MapType; 7 | paintData: StylePaintData; 8 | }; 9 | 10 | const getColorExpression = ( 11 | property: string, 12 | paintData: StylePaintData 13 | ): Expression => { 14 | return [ 15 | 'match', 16 | ['get', property], 17 | 'no', 18 | paintData.schoolConnectivity.no, 19 | 'unknown', 20 | paintData.schoolConnectivity.unknown, 21 | 'moderate', 22 | paintData.schoolConnectivity.moderate, 23 | 'good', 24 | paintData.schoolConnectivity.good, 25 | 'notVerified', 26 | paintData.schoolConnectivity.notVerified, 27 | paintData.schoolConnectivity.unknown, 28 | ]; 29 | }; 30 | 31 | export const getSchoolsColors = ({ 32 | mapType, 33 | paintData, 34 | }: GetSchoolsColors): string | StyleFunction | Expression | undefined => { 35 | if (mapType === 'connectivity') { 36 | return getColorExpression('connectivity_status', paintData); 37 | } 38 | 39 | if (mapType === 'coverage') { 40 | return getColorExpression('coverage_status', paintData); 41 | } 42 | 43 | return '#ffffff'; 44 | }; 45 | -------------------------------------------------------------------------------- /src/@/country/effects/index.ts: -------------------------------------------------------------------------------- 1 | export { addCountriesFx } from './add-countries-fx'; 2 | export { leaveCountryRouteFx } from './leave-country-route-fx'; 3 | export { updateCountryFx } from './update-country-fx'; 4 | export { updateSchoolsFx } from './update-schools-fx'; 5 | export { zoomToCountryFx } from './zoom-to-country-fx'; 6 | export { updateSchoolsColorsFx } from './update-schools-colors-fx'; 7 | export { removeCountryFx } from './remove-country-fx'; 8 | export { removeSchoolsFx } from './remove-schools-fx'; 9 | export { updateGlobalSchoolsFx } from './update-global-schools-fx'; 10 | -------------------------------------------------------------------------------- /src/@/country/effects/leave-country-route-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | // eslint-disable-next-line no-restricted-imports 4 | import { addCountriesLayer } from '@/country/effects/add-countries-layer'; 5 | // eslint-disable-next-line no-restricted-imports 6 | import { addGlobalSchoolsLayer } from '@/country/effects/add-global-schools-layer'; 7 | import { LeaveCountryRoute } from '@/country/types'; 8 | import { defaultCenter, defaultZoom } from '@/map/constants'; 9 | 10 | import { removeCountryFx } from './remove-country-fx'; 11 | import { removeSchoolsFx } from './remove-schools-fx'; 12 | 13 | export const leaveCountryRouteFx = createEffect( 14 | async ({ map, paintData, popup }: LeaveCountryRoute) => { 15 | if (!map) return; 16 | 17 | popup?.remove(); 18 | 19 | map.flyTo({ 20 | center: defaultCenter, 21 | zoom: defaultZoom, 22 | }); 23 | 24 | addGlobalSchoolsLayer({ map, paintData }); 25 | addCountriesLayer({ map, paintData }); 26 | 27 | await Promise.all([ 28 | removeSchoolsFx(map), 29 | removeCountryFx({ map, paintData }), 30 | ]); 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /src/@/country/effects/remove-country-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | import { clickSchool } from '@/country/model'; 4 | import { getDefaultCountryOpacity } from '@/map/constants'; 5 | import { Map, StylePaintData } from '@/map/types'; 6 | 7 | export const removeCountryFx = createEffect( 8 | ({ map, paintData }: { map: Map | null; paintData: StylePaintData }) => { 9 | if (!map) return; 10 | 11 | map.off('click', 'selectedCountry', clickSchool); 12 | 13 | if (map.getLayer('selectedCountry')) { 14 | map.removeLayer('selectedCountry'); 15 | } 16 | 17 | if (map.getSource('selectedCountry')) { 18 | map.removeSource('selectedCountry'); 19 | } 20 | if (map.getLayer('countries')) { 21 | map.setPaintProperty( 22 | 'countries', 23 | 'fill-opacity', 24 | getDefaultCountryOpacity(paintData) 25 | ); 26 | } 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /src/@/country/effects/remove-schools-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | import { Map } from '@/map/types'; 4 | 5 | export const removeSchoolsFx = createEffect((map: Map) => { 6 | map.off('mouseenter', 'schools', () => { 7 | // eslint-disable-next-line no-param-reassign 8 | map.getCanvas().style.cursor = 'pointer'; 9 | }); 10 | map.off('mouseleave', 'schools', () => { 11 | // eslint-disable-next-line no-param-reassign 12 | map.getCanvas().style.cursor = ''; 13 | }); 14 | 15 | if (map.getLayer('schools')) { 16 | map.removeLayer('schools'); 17 | } 18 | 19 | if (map.getSource('schools')) { 20 | map.removeSource('schools'); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/@/country/effects/update-country-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | import { clickSchool } from '@/country/model'; 4 | import { UpdateCountry } from '@/country/types'; 5 | 6 | export const updateCountryFx = createEffect( 7 | ({ map, paintData, country }: UpdateCountry) => { 8 | if (!country || !map) return; 9 | 10 | map.addSource('selectedCountry', { 11 | type: 'geojson', 12 | data: { 13 | type: 'Feature', 14 | geometry: country.geometry, 15 | properties: {}, 16 | }, 17 | }); 18 | 19 | map.addLayer( 20 | { 21 | id: 'selectedCountry', 22 | type: 'fill', 23 | source: 'selectedCountry', 24 | paint: { 25 | 'fill-color': paintData.countrySelected, 26 | 'fill-opacity': paintData.selectedCountryOpacity, 27 | 'fill-outline-color': paintData.background, 28 | }, 29 | }, 30 | // Country layer always below schools layer 31 | map.getLayer('schools') ? 'schools' : '' 32 | ); 33 | 34 | map.on('click', 'selectedCountry', clickSchool); 35 | 36 | if (map.getLayer('countries')) { 37 | map.removeLayer('countries'); 38 | } 39 | if (map.getLayer('boundaries')) { 40 | map.removeLayer('boundaries'); 41 | } 42 | if (map.getLayer('schoolsGlobal')) { 43 | map.removeLayer('schoolsGlobal'); 44 | } 45 | } 46 | ); 47 | -------------------------------------------------------------------------------- /src/@/country/effects/update-global-schools-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | // eslint-disable-next-line no-restricted-imports 4 | import { addGlobalSchoolsLayer } from '@/country/effects/add-global-schools-layer'; 5 | import { UpdateGlobalSchools } from '@/country/types'; 6 | 7 | export const updateGlobalSchoolsFx = createEffect( 8 | ({ map, paintData, schoolsGlobal, countryCode }: UpdateGlobalSchools) => { 9 | if (!map || !schoolsGlobal) return; 10 | 11 | map.addSource('schoolsGlobal', { 12 | type: 'geojson', 13 | data: schoolsGlobal, 14 | }); 15 | 16 | if (!countryCode) { 17 | addGlobalSchoolsLayer({ map, paintData }); 18 | } 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /src/@/country/effects/update-schools-colors-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | import { UpdateSchoolsColors } from '@/country/types'; 4 | 5 | import { getSchoolsColors } from './get-schools-colors'; 6 | 7 | export const updateSchoolsColorsFx = createEffect( 8 | ({ map, mapType, paintData }: UpdateSchoolsColors) => { 9 | if (!map) return; 10 | 11 | if (map.getLayer('schools')) { 12 | map.setPaintProperty( 13 | 'schools', 14 | 'circle-color', 15 | getSchoolsColors({ 16 | mapType, 17 | paintData, 18 | }) 19 | ); 20 | } 21 | } 22 | ); 23 | -------------------------------------------------------------------------------- /src/@/country/effects/update-schools-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | import { UpdateSchools } from '@/country/types'; 4 | import { mapCircleSizes } from '@/map/constants'; 5 | 6 | import { getSchoolsColors } from './get-schools-colors'; 7 | 8 | export const updateSchoolsFx = createEffect( 9 | ({ map, schools, mapType, paintData }: UpdateSchools) => { 10 | if (!map || !schools) return; 11 | 12 | if (schools.features.length > 0) { 13 | map.addSource('schools', { 14 | type: 'geojson', 15 | data: schools, 16 | }); 17 | 18 | map.addLayer({ 19 | id: 'schools', 20 | type: 'circle', 21 | source: 'schools', 22 | paint: { 23 | 'circle-radius': { 24 | stops: mapCircleSizes, 25 | }, 26 | 'circle-color': getSchoolsColors({ 27 | mapType, 28 | paintData, 29 | }), 30 | }, 31 | }); 32 | 33 | map.on('mouseenter', 'schools', () => { 34 | // eslint-disable-next-line no-param-reassign 35 | map.getCanvas().style.cursor = 'pointer'; 36 | }); 37 | 38 | map.on('mouseleave', 'schools', () => { 39 | // eslint-disable-next-line no-param-reassign 40 | map.getCanvas().style.cursor = ''; 41 | }); 42 | } 43 | } 44 | ); 45 | -------------------------------------------------------------------------------- /src/@/country/effects/zoom-to-country-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | import { MultiPolygon, Polygon } from 'geojson'; 3 | 4 | import { getPolygonBoundingBox } from '@/country/lib'; 5 | import { ZoomToCountryBounds } from '@/country/types'; 6 | 7 | const paddingMobile = { 8 | left: 5, 9 | right: 5, 10 | top: 50, 11 | bottom: 5, 12 | }; 13 | 14 | const paddingDesktop = { 15 | left: 360, 16 | right: 30, 17 | top: 30, 18 | bottom: 30, 19 | }; 20 | 21 | export const zoomToCountryFx = createEffect( 22 | ({ 23 | map, 24 | countriesGeometry, 25 | countryCode, 26 | country, 27 | isMobile, 28 | }: ZoomToCountryBounds): string => { 29 | if (!countryCode || !map) return ''; 30 | 31 | const currentCountryGeometry = 32 | countriesGeometry?.find( 33 | (countryGeometry) => 34 | countryGeometry.code.toLocaleLowerCase() === 35 | countryCode.toLocaleLowerCase() 36 | )?.geometry_simplified ?? country?.geometry; 37 | 38 | if (currentCountryGeometry) { 39 | const bounds = getPolygonBoundingBox( 40 | currentCountryGeometry as Polygon | MultiPolygon 41 | ); 42 | 43 | map.fitBounds(bounds, { 44 | padding: isMobile ? paddingMobile : paddingDesktop, 45 | }); 46 | 47 | return countryCode; 48 | } 49 | 50 | return ''; 51 | } 52 | ); 53 | -------------------------------------------------------------------------------- /src/@/country/lib/get-countries-geo-json.ts: -------------------------------------------------------------------------------- 1 | import { FeatureCollection, Geometry } from 'geojson'; 2 | 3 | import { CountryBasic, CountryGeometry } from '~/api/types'; 4 | 5 | export const getCountriesGeoJson = ( 6 | countriesProperties: CountryBasic[] | null, 7 | countriesGeometry: CountryGeometry[] | null 8 | ): FeatureCollection => { 9 | return { 10 | type: 'FeatureCollection', 11 | features: 12 | countriesProperties 13 | ?.filter( 14 | (country) => 15 | country.integration_status !== 0 && country.integration_status !== 4 16 | ) 17 | .map((country) => { 18 | const countryGeometry = countriesGeometry?.find( 19 | (geometry) => geometry.id === country.id 20 | ); 21 | return { 22 | type: 'Feature', 23 | properties: { 24 | integration_status: country.integration_status, 25 | code: country.code, 26 | }, 27 | geometry: countryGeometry?.geometry_simplified as Geometry, 28 | id: country.id, 29 | }; 30 | }) ?? [], 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/@/country/lib/get-global-schools-geo-json.ts: -------------------------------------------------------------------------------- 1 | import { FeatureCollection } from 'geojson'; 2 | 3 | import { SchoolSimplified } from '~/api/types'; 4 | 5 | export const getGlobalSchoolsGeoJson = ( 6 | points: SchoolSimplified[] 7 | ): FeatureCollection => { 8 | return { 9 | type: 'FeatureCollection', 10 | features: points.map((point) => ({ 11 | type: 'Feature', 12 | geometry: point.geopoint, 13 | properties: { 14 | country_integration_status: point.country_integration_status, 15 | }, 16 | })), 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/@/country/lib/get-polygon-bounding-box.ts: -------------------------------------------------------------------------------- 1 | import { MultiPolygon, Polygon } from 'geojson'; 2 | import { LngLatLike } from 'mapbox-gl'; 3 | 4 | // noinspection FunctionWithMultipleLoopsJS 5 | export const getPolygonBoundingBox = ( 6 | geometry: Polygon | MultiPolygon 7 | ): [LngLatLike, LngLatLike] => { 8 | // Longitude -180 - 180 9 | // Latitude -90 - 90 10 | let maxLng = -180; 11 | let minLng = 180; 12 | let maxLat = -90; 13 | let minLat = 90; 14 | 15 | for (const coordinates of geometry.coordinates) { 16 | const polygon = 17 | typeof coordinates[0][0] === 'number' ? coordinates : coordinates[0]; 18 | 19 | for (const element of polygon) { 20 | const [longitude, latitude] = element as [number, number]; 21 | minLng = Math.min(minLng, longitude); 22 | maxLng = Math.max(maxLng, longitude); 23 | minLat = Math.min(minLat, latitude); 24 | maxLat = Math.max(maxLat, latitude); 25 | } 26 | } 27 | // Bounds [xMin, yMin][xMax, yMax] 28 | return [ 29 | [minLng, minLat], 30 | [maxLng, maxLat], 31 | ]; 32 | }; 33 | -------------------------------------------------------------------------------- /src/@/country/lib/get-schools-geojson.ts: -------------------------------------------------------------------------------- 1 | import { FeatureCollection } from 'geojson'; 2 | 3 | import { SchoolBasic } from '~/api/types'; 4 | 5 | export const getSchoolsGeoJson = (points: SchoolBasic[]): FeatureCollection => { 6 | return { 7 | type: 'FeatureCollection', 8 | features: points.map((point) => ({ 9 | type: 'Feature', 10 | properties: { 11 | name: point.name, 12 | connectivity_status: point.is_verified 13 | ? point.connectivity_status 14 | : 'notVerified', 15 | coverage_status: point.is_verified 16 | ? point.coverage_status 17 | : 'notVerified', 18 | }, 19 | geometry: point.geopoint, 20 | id: point.id, 21 | })), 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/@/country/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { getCountriesGeoJson } from './get-countries-geo-json'; 2 | export { getPolygonBoundingBox } from './get-polygon-bounding-box'; 3 | export { getSchoolsGeoJson } from './get-schools-geojson'; 4 | export { getGlobalSchoolsGeoJson } from './get-global-schools-geo-json'; 5 | -------------------------------------------------------------------------------- /src/@/country/model.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from 'effector'; 2 | import { FeatureCollection } from 'geojson'; 3 | import { MapMouseEvent } from 'mapbox-gl'; 4 | 5 | import { checkSchoolHasHistory, fetchSchoolFx } from '~/api/project-connect'; 6 | import { 7 | Country, 8 | CountryBasic, 9 | CountryGeometry, 10 | CountryWeeklyStats, 11 | DailyStats, 12 | School, 13 | } from '~/api/types'; 14 | 15 | export const changeCountryCode = createEvent(); 16 | export const changeSchoolId = createEvent(); 17 | export const clickSchool = createEvent(); 18 | 19 | export const $countryCode = createStore(''); 20 | export const $schoolId = createStore(0); 21 | export const $countriesGeometry = createStore(null); 22 | export const $countries = createStore(null); 23 | export const $countriesGeoJson = createStore(null); 24 | export const $schoolsGlobal = createStore(null); 25 | export const $schools = createStore(null); 26 | export const $country = createStore(null); 27 | export const $school = createStore(null); 28 | export const $schoolHasHistory = createStore(false); 29 | export const $schoolPending = 30 | fetchSchoolFx.pending || checkSchoolHasHistory.pending; 31 | export const $countryWeeklyStats = createStore(null); 32 | export const $countryDailyStats = createStore(null); 33 | export const $schoolDailyStats = createStore(null); 34 | export const $zoomedCountryCode = createStore(''); 35 | export const $countryInfoPending = createStore(false); 36 | export const $countryHasConnectivity = createStore(false); 37 | export const $countryHasCoverage = createStore(false); 38 | -------------------------------------------------------------------------------- /src/@/country/types.ts: -------------------------------------------------------------------------------- 1 | import { FeatureCollection } from 'geojson'; 2 | import type mapboxGl from 'mapbox-gl'; 3 | 4 | import { Country, CountryGeometry } from '~/api/types'; 5 | 6 | import { Map, MapType, StylePaintData } from '@/map/types'; 7 | 8 | export type UpdateCountry = { 9 | map: Map | null; 10 | paintData: StylePaintData; 11 | country: Country | null; 12 | }; 13 | 14 | export type UpdateSchools = { 15 | map: Map | null; 16 | schools: FeatureCollection | null; 17 | mapType: MapType; 18 | paintData: StylePaintData; 19 | }; 20 | 21 | export type UpdateGlobalSchools = { 22 | map: Map | null; 23 | paintData: StylePaintData; 24 | schoolsGlobal: FeatureCollection | null; 25 | countryCode: string; 26 | }; 27 | 28 | export type UpdateSchoolsColors = { 29 | map: Map | null; 30 | mapType: MapType; 31 | paintData: StylePaintData; 32 | }; 33 | 34 | export type ZoomToCountryBounds = { 35 | map: Map | null; 36 | countryCode: string; 37 | countriesGeometry: CountryGeometry[] | null; 38 | country: Country | null; 39 | isMobile: boolean; 40 | }; 41 | 42 | export type LeaveCountryRoute = { 43 | map: Map | null; 44 | paintData: StylePaintData; 45 | popup: mapboxGl.Popup | null; 46 | }; 47 | 48 | export type AddCountries = { 49 | map: Map | null; 50 | paintData: StylePaintData; 51 | countriesGeoJson: FeatureCollection | null; 52 | countryCode: string; 53 | }; 54 | -------------------------------------------------------------------------------- /src/@/dashboard/constats.ts: -------------------------------------------------------------------------------- 1 | export const defaultSortKey = 'countryProgress'; 2 | -------------------------------------------------------------------------------- /src/@/dashboard/model.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from 'effector'; 2 | 3 | import { CountryBasic } from '~/api/types'; 4 | 5 | import { defaultSortKey } from '@/dashboard/constats'; 6 | import { SortKey } from '@/sidebar/types'; 7 | 8 | import { Tabs } from './types'; 9 | 10 | export const changeSearchText = createEvent(); 11 | export const changeViewType = createEvent(); 12 | export const changePopupStatus = createEvent(); 13 | export const clearSearchText = createEvent(); 14 | export const changeSortKey = createEvent(); 15 | export const changeDashboardCountryId = createEvent(); 16 | export const changeControlsSortKey = createEvent(); 17 | export const submitControlsChanges = createEvent(); 18 | 19 | export const $searchText = createStore(''); 20 | export const $hasSearchText = createStore(false); 21 | export const $countriesList = createStore([]); 22 | export const $dashboardCountryId = createStore(0); 23 | export const $noSearchResults = createStore(false); 24 | export const $isListType = createStore(false); 25 | export const $isPopupOpen = createStore(false); 26 | export const $isLoading = createStore(false); 27 | export const $sortKey = createStore(defaultSortKey); 28 | export const $controlsSortKey = createStore(defaultSortKey); 29 | export const $isControlsChanged = createStore(false); 30 | 31 | export const selectCountriesTab = createEvent(); 32 | export const selectSortTab = createEvent(); 33 | export const $tab = createStore('countries'); 34 | export const $isCountriesTab = createStore(true); 35 | export const $isSortTab = createStore(false); 36 | -------------------------------------------------------------------------------- /src/@/dashboard/types.ts: -------------------------------------------------------------------------------- 1 | type ProgressBar = { 2 | width: string; 3 | }; 4 | 5 | type MapPreview = { 6 | backgroundImage: string; 7 | }; 8 | 9 | export type CountryInfo = { 10 | id: number; 11 | code: string; 12 | flag: string; 13 | name: string; 14 | joinDate: string; 15 | progressPercent: number; 16 | progressBarStyle: ProgressBar; 17 | bubbleProgressClass: string; 18 | progressDescription: string; 19 | mapPreviewStyle: MapPreview; 20 | }; 21 | 22 | export type Tabs = 'countries' | 'sort'; 23 | -------------------------------------------------------------------------------- /src/@/dashboard/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { Dashboard } from './dashboard'; 2 | -------------------------------------------------------------------------------- /src/@/dashboard/ui/search-sort-results.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'effector-react'; 2 | import React from 'react'; 3 | 4 | import { getInverted } from '~/lib/effector-kit'; 5 | 6 | import { 7 | $isPopupOpen, 8 | $noSearchResults, 9 | changePopupStatus, 10 | } from '@/dashboard/model'; 11 | 12 | import { CountryList } from './country-list'; 13 | import { CountryPopupDetails } from './county-popup'; 14 | 15 | export const NotFound = () =>

Countries not found

; 16 | // View 17 | const onChangeView = changePopupStatus.prepend(getInverted); 18 | 19 | const CountriesFound = () => { 20 | const isPopupOpen = useStore($isPopupOpen); 21 | 22 | return isPopupOpen ? ( 23 |
24 | 31 | 32 |
33 | ) : ( 34 | 35 | ); 36 | }; 37 | 38 | export const SearchResults = () => ( 39 | <>{useStore($noSearchResults) ? : } 40 | ); 41 | -------------------------------------------------------------------------------- /src/@/history-modal/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { getHistoryGraphData } from './get-history-graph-data'; 2 | -------------------------------------------------------------------------------- /src/@/history-modal/model.ts: -------------------------------------------------------------------------------- 1 | import { Interval } from 'date-fns'; 2 | import { createEvent, createStore } from 'effector'; 3 | 4 | import { DailyStats } from '~/api/types'; 5 | import { IntervalUnit } from '~/lib/date-fns-kit/types'; 6 | 7 | import { defaultInterval } from '@/sidebar/constants'; 8 | 9 | import { StatsDataType } from './types'; 10 | 11 | export const changeHistoryDataType = createEvent(); 12 | export const closeHistoryModal = createEvent(); 13 | export const changeHistoryIntervalUnit = createEvent(); 14 | export const nextHistoryInterval = createEvent(); 15 | export const previousHistoryInterval = createEvent(); 16 | 17 | export const $historyDataType = createStore(null); 18 | export const $isOpenHistoryModal = createStore(false); 19 | export const $historyIntervalUnit = createStore('week'); 20 | export const $historyInterval = createStore(defaultInterval); 21 | export const $isCurrentHistoryInterval = createStore(true); 22 | export const $isNextHistoryIntervalAvailable = createStore(false); 23 | export const $isPreviousHistoryIntervalAvailable = createStore(false); 24 | export const $historyData = createStore(null); 25 | export const $historyDataPending = createStore(false); 26 | export const $historyPlaceName = createStore(null); 27 | -------------------------------------------------------------------------------- /src/@/history-modal/types.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionStatus } from '~/api/types'; 2 | 3 | export type StatsDataType = 'country' | 'school'; 4 | 5 | export type HistoryGraphData = { 6 | daysData: HistoryGraphItem[]; 7 | speedSum: number; 8 | itemsCount: number; 9 | maxSpeed: number; 10 | }; 11 | 12 | export type HistoryGraphItem = { 13 | date: string; 14 | speedFormatted?: string; 15 | speed?: number; 16 | status?: ConnectionStatus; 17 | }; 18 | -------------------------------------------------------------------------------- /src/@/history-modal/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { HistoryModal } from './history-modal'; 2 | -------------------------------------------------------------------------------- /src/@/map/effects/add-loader-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | import mapboxGL from 'mapbox-gl'; 3 | 4 | import { setLoader } from '@/map/model'; 5 | import { Map } from '@/map/types'; 6 | 7 | export const addLoaderToMapFx = createEffect((map: Map | null) => { 8 | if (!map) return; 9 | 10 | // Create loader (should be wrapped in a container) 11 | const loader = document.createElement('div'); 12 | loader.className = 'map-loader'; 13 | const loaderWrapper = document.createElement('div'); 14 | loaderWrapper.append(loader); 15 | 16 | // Add loader 17 | const loaderMarker = new mapboxGL.Marker(loaderWrapper) 18 | .setLngLat(map.getCenter()) 19 | .addTo(map); 20 | 21 | // Always display the loader in the center 22 | map.on('zoom', () => { 23 | if (loaderMarker) { 24 | loaderMarker.setLngLat(map.getCenter()); 25 | } 26 | }); 27 | 28 | map.on('move', () => { 29 | if (loaderMarker) { 30 | loaderMarker.setLngLat(map.getCenter()); 31 | } 32 | }); 33 | 34 | setLoader(loaderMarker); 35 | }); 36 | -------------------------------------------------------------------------------- /src/@/map/effects/change-style-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | import { styleUrls } from '@/map/constants'; 4 | import { Map, Style } from '@/map/types'; 5 | 6 | export const changeStyleFx = createEffect( 7 | ({ map, style }: { map: Map; style: Style }) => { 8 | map.setStyle(styleUrls[style]); 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /src/@/map/effects/index.ts: -------------------------------------------------------------------------------- 1 | export { addLoaderToMapFx } from './add-loader-fx'; 2 | export { initMapFx } from './init-map-fx'; 3 | export { removeLoaderFromMapFx } from './remove-loader-fx'; 4 | export { changeStyleFx } from './change-style-fx'; 5 | -------------------------------------------------------------------------------- /src/@/map/effects/init-map-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | import mapboxGL from 'mapbox-gl'; 3 | 4 | import { API_MAPBOX_ACCESS_TOKEN } from '~/env'; 5 | 6 | import { defaultCenter, defaultZoom, styleUrls } from '@/map/constants'; 7 | import { changeMap, onStyleLoaded } from '@/map/model'; 8 | import { InitMapOptions } from '@/map/types'; 9 | 10 | export const initMapFx = createEffect( 11 | ({ style, container, center, zoom }: InitMapOptions) => { 12 | mapboxGL.accessToken = API_MAPBOX_ACCESS_TOKEN; 13 | 14 | const map = new mapboxGL.Map({ 15 | style: styleUrls[style], 16 | center: center ?? defaultCenter, 17 | zoom: zoom ?? defaultZoom, 18 | container, 19 | }); 20 | 21 | map.on('load', () => { 22 | changeMap(map); 23 | }); 24 | 25 | map.on('styledata', () => { 26 | onStyleLoaded(); 27 | }); 28 | } 29 | ); 30 | -------------------------------------------------------------------------------- /src/@/map/effects/remove-loader-fx.ts: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'effector'; 2 | 3 | import { Marker } from '@/map/types'; 4 | 5 | export const removeLoaderFromMapFx = createEffect((loader: Marker | null) => { 6 | loader?.remove(); 7 | }); 8 | -------------------------------------------------------------------------------- /src/@/map/model.ts: -------------------------------------------------------------------------------- 1 | import { createEvent, createStore } from 'effector'; 2 | 3 | import { GlobalStats } from '~/api/types'; 4 | 5 | import { 6 | defaultGlobalStats, 7 | defaultMapType, 8 | defaultStyle, 9 | stylePaintData, 10 | } from './constants'; 11 | import { Center, Map, MapType, Marker, Style, StylePaintData } from './types'; 12 | 13 | export const changeMap = createEvent(); 14 | export const changeMapType = createEvent(); 15 | export const changeStyle = createEvent